本文將介紹在Linux系統(tǒng)中,以一個UDP包的接收過程作為示例,介紹數據包是如何一步一步從網卡傳到進程手中的。
網卡到內存
網絡接口卡必須安裝與之匹配的驅動程序才能正常工作。這些驅動程序被視為內核模塊,其主要職責是連接網卡和內核中的網絡模塊。在加載驅動程序時,驅動程序將自身注冊到網絡模塊中。當相應的網卡接收到數據包時,網絡模塊將調用相應的驅動程序來處理數據。
下圖展示了數據包(packet)如何進入內存,并被內核的網絡模塊開始處理:
- 1:外部網絡傳入的數據包會進入物理網卡。當目的地址不屬于該網卡,且該網卡未啟用混雜模式時,該數據包將被網卡丟棄。2:網卡使用直接內存訪問(DMA)技術將數據包寫入指定的內存地址。這些內存地址由網卡驅動程序進行分配和初始化。3:網卡通過硬件中斷請求(IRQ)向CPU發(fā)送通知,以告知數據已到達。4:CPU根據中斷表的配置,調用已注冊的中斷處理函數,該函數會進一步調用網卡驅動程序(網絡接口卡驅動程序)中相應的函數。5:驅動程序首先禁用網卡的中斷功能,表示驅動程序已知曉數據已存儲在內存中,并告知網卡在接收到下一個數據包時直接寫入內存,而無需再次通知CPU,從而提高效率,并避免CPU被頻繁中斷。6:啟動軟中斷。硬中斷處理函數執(zhí)行期間不可被中斷,若其執(zhí)行時間過長,則會導致CPU無法響應其他硬件的中斷。因此,內核引入軟中斷的概念,將硬中斷處理函數中耗時的部分轉移到軟中斷處理函數中,以便逐步處理。
內核的網絡模塊
軟中斷會觸發(fā)內核網絡模塊中的軟中斷處理函數,后續(xù)流程如下:
- 7:在操作系統(tǒng)內核中,存在一個專門處理軟中斷的進程,稱為ksoftirqd。當ksoftirqd接收到軟中斷時,它會調用相應的軟中斷處理函數,對于上述提到的第6步中由網卡驅動模塊觸發(fā)的軟中斷,ksoftirqd會調用網絡模塊中的net_rx_action函數。8:net_rx_action函數會調用網卡驅動中的poll函數,逐個處理數據包。9:在poll函數中,驅動程序會逐個讀取網卡寫入內存的數據包,該數據包的格式只有驅動程序知道。10:驅動程序將內存中的數據包轉換為內核網絡模塊可識別的skb格式,并調用napi_gro_receive函數。11:napi_gro_receive函數會處理與GRO(通用接收處理)相關的內容,即將可合并的數據包進行合并,從而只需調用一次協(xié)議棧。然后檢查是否啟用了RPS(接收包分發(fā)),若啟用,則調用enqueue_to_backlog函數。12:在enqueue_to_backlog函數中,數據包將被放入CPU的softnet_data結構體的input_pkt_queue隊列中,然后返回。如果input_pkt_queue隊列已滿,則會丟棄該數據包,該隊列的大小可以通過net.core.netdev_max_backlog參數進行配置。13:CPU會在自身的軟中斷上下文中處理input_pkt_queue隊列中的網絡數據(調用__netif_receive_skb_core函數)。14:如果未啟用RPS,napi_gro_receive函數會直接調用__netif_receive_skb_core函數。15:首先檢查是否存在AF_PACKET類型的套接字(即原始套接字),如果存在,則將數據包復制給該套接字。例如,tcpdump抓取的數據包即是在此處捕獲的。16:調用相應的協(xié)議棧函數,將數據包交給協(xié)議棧處理。17:在內存中的所有數據包處理完成后(即poll函數執(zhí)行完成),啟用網卡的硬中斷,這樣當網卡接收到下一批數據時,將會通知CPU。
enqueue_to_backlog函數也會被netif_rx函數調用,而netif_rx正是lo設備發(fā)送數據包時調用的函數
協(xié)議棧
IP層
由于是UDP包,所以第一步會進入IP層,然后一級一級的函數往下調:
- ip_rcv:ip_rcv函數是IP模塊的入口函數,在該函數里面,第一件事就是將垃圾數據包(目的mac地址不是當前網卡,但由于網卡設置了混雜模式而被接收進來)直接丟掉,然后調用注冊在NF_INET_PRE_ROUTING上的函數NF_INET_PRE_ROUTING:netfilter放在協(xié)議棧中的鉤子,可以通過iptables來注入一些數據包處理函數,用來修改或者丟棄數據包,如果數據包沒被丟棄,將繼續(xù)往下走routing:進行路由,如果目的IP不是本地IP,且沒有開啟ip forward功能,那么數據包將被丟棄,如果開啟了ip forward功能,那將進入ip_forward函數ip_forward:ip_forward會先調用netfilter注冊的NF_INET_FORWARD相關函數,如果數據包沒有被丟棄,那么將繼續(xù)往后調用dst_output_sk函數dst_output_sk:該函數會調用IP層的相應函數將該數據包發(fā)送出去。ip_local_deliver:如果上面routing的時候發(fā)現(xiàn)目的IP是本地IP,那么將會調用該函數,在該函數中,會先調用NF_INET_LOCAL_IN相關的鉤子程序,如果通過,數據包將會向下發(fā)送到UDP層
UDP層
- udp_rcv函數是UDP模塊的入口函數,用于處理接收到的UDP數據包。在該函數中會進行一系列檢查,并調用其他函數進行處理。其中,一個重要的函數調用是__udp4_lib_lookup_skb,該函數根據目標IP和端口查找對應的socket。如果找不到相應的socket,則該數據包將被丟棄;否則,繼續(xù)處理。sock_queue_rcv_skb函數的主要功能是進行兩項檢查。首先,它會檢查socket的接收緩沖區(qū)是否已滿,如果已滿,則會丟棄該數據包。然后,它會調用sk_filter函數檢查該包是否滿足當前socket設置的過濾條件。如果socket上設置了過濾條件且該數據包不滿足條件,則該數據包也會被丟棄。在Linux中,每個socket都可以像tcpdump中一樣定義過濾條件,不滿足條件的數據包將被丟棄。__skb_queue_tail函數用于將數據包放入socket的接收隊列末尾。sk_data_ready函數用于通知socket數據包已準備就緒,可以進行處理。
調用完sk_data_ready之后,一個數據包處理完成,等待應用層程序來讀取,上面所有函數的執(zhí)行過程都在軟中斷的上下文中。
socket
應用層一般有兩種方式接收數據,一種是recvfrom函數阻塞在那里等著數據來,這種情況下當socket收到通知后,recvfrom就會被喚醒,然后讀取接收隊列的數據;另一種是通過epoll或者select監(jiān)聽相應的socket,當收到通知后,再調用recvfrom函數去讀取接收隊列的數據。兩種情況都能正常的接收到相應的數據包。
結束語
了解數據包的接收流程有助于幫助我們搞清楚我們可以在哪些地方監(jiān)控和修改數據包,哪些情況下數據包可能被丟棄,為我們處理網絡問題提供了一些參考,同時了解netfilter中相應鉤子的位置,對于了解iptables的用法有一定的幫助。