Linux網絡包收發總體過程
就TCP/IP而言,IP和TCP的報文結構並不是最重要的,但是很多文章都在討論他們,就體系而言,最重要的應該是各棧的流轉流程。如果應用的話,重點應該在4次揮手(tcp的三次握手與四次揮手及為什么面試官喜歡問這個問題)及粘包和拆包及滑動窗口等。下面簡單看下整體的收發過程。

注:Socket是提供給用戶訪問的TCP層接口,應用層的數據收發都在socket緩沖區中。 對應的數據流和控制流如下:

網卡到內存
網卡需要有驅動才能工作,驅動是加載到內核中的模塊,負責銜接網卡和內核的網絡模塊,驅動在加載的時候將自己注冊進網絡模塊,當相應的網卡收到數據包時,網絡模塊會調用相應的驅動程序處理數據。
下圖展示了數據包(packet)如何進入內存,並被內核的網絡模塊開始處理:

- 1: 數據包從外面的網絡進入物理網卡。如果目的地址不是該網卡,且該網卡沒有開啟混雜模式,該包會被網卡丟棄。
- 2: 網卡將數據包通過DMA(或DMA)的方式寫入到指定的內存地址,該地址由網卡驅動分配並初始化。注: 老的網卡可能不支持DMA,不過新的網卡一般都支持,具體多少划給DMA使用,不同的計算機體系有所不同,很多體系全部內存都可用。
- 3: 網卡通過硬件中斷(IRQ)通知CPU,告訴它有數據來了
- 4: CPU根據中斷表,調用已經注冊的中斷函數,這個中斷函數會調到驅動程序(NIC Driver)中相應的函數
- 5: 驅動先禁用網卡的中斷,表示驅動程序已經知道內存中有數據了,告訴網卡下次再收到數據包直接寫內存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
- 6: 啟動軟中斷。這步結束后,硬件中斷處理函數就結束返回了。由於硬中斷處理程序執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬件的中斷,於是內核引入軟中斷,這樣可以將硬中斷處理函數中耗時的部分移到軟中斷處理函數里面來慢慢處理。
內核的網絡模塊(驅動到IP棧處理前)
軟中斷會觸發內核網絡模塊中的軟中斷處理函數,后續流程如下:

- 7: 內核中的ksoftirqd進程專門負責軟中斷的處理,當它收到軟中斷后,就會調用相應軟中斷所對應的處理函數,對於上面第6步中是網卡驅動模塊拋出的軟中斷,ksoftirqd會調用網絡模塊的net_rx_action函數
- 8: net_rx_action調用網卡驅動里的poll函數來一個一個的處理數據包
- 9: 在pool函數中,驅動會一個接一個的讀取網卡寫到內存中的數據包,內存中數據包的格式只有驅動知道
- 10: 驅動程序將內存中的數據包轉換成內核網絡模塊能識別的skb格式,然后調用napi_gro_receive函數
- 11: napi_gro_receive會處理GRO相關的內容,也就是將可以合並的數據包進行合並,這樣就只需要調用一次協議棧。然后判斷是否開啟了RPS,如果開啟了,將會調用enqueue_to_backlog
- 12: 在enqueue_to_backlog函數中,會將數據包放入CPU的softnet_data結構體的input_pkt_queue中,然后返回,如果input_pkt_queue滿了的話,該數據包將會被丟棄,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類型的socket(也就是我們常說的原始套接字),如果有的話,拷貝一份數據給它。tcpdump抓包就是抓的這里的包。
- 16: 調用協議棧相應的函數,將數據包交給協議棧處理,通常即IP協議棧。
- 17: 待內存中的所有數據包被處理完成后(即poll函數執行完成),啟用網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知CPU
enqueue_to_backlog函數也會被netif_rx函數調用,而netif_rx正是lo設備發送數據包時調用的函數
協議棧-IP層
無論是TCP還是UDP包,所以第一步會進入IP層,然后一級一級的函數往下調:

- ip_rcv: ip_rcv函數是IP模塊的入口函數,在該函數里面,第一件事就是將垃圾數據包(目的mac地址不是當前網卡,但由於網卡設置了混雜模式而被接收進來)直接丟掉,然后調用注冊在NF_INET_PRE_ROUTING上的函數
- NF_INET_PRE_ROUTING: netfilter放在協議棧中的鈎子,可以通過iptables來注入一些數據包處理函數,用來修改或者丟棄數據包,如果數據包沒被丟棄,將繼續往下走
- routing: 進行路由,如果是目的IP不是本地IP,且沒有開啟ip forward功能,那么數據包將被丟棄,如果開啟了ip forward功能,那將進入ip_forward函數
- ip_forward: ip_forward會先調用netfilter注冊的NF_INET_FORWARD相關函數,如果數據包沒有被丟棄,那么將繼續往后調用dst_output_sk函數
- dst_output_sk: 該函數會調用IP層的相應函數將該數據包發送出去,同下一篇要介紹的數據包發送流程的后半部分一樣。
- ip_local_deliver:如果上面routing的時候發現目的IP是本地IP,那么將會調用該函數,該函數會先進行必要的組包(因為IP層負責包拆分為MTU大小),然后調用NF_INET_LOCAL_IN相關的鈎子程序,如果通過,數據包將會向下發送到TCP或UDP層。其過程如下:
/* * Deliver IP Packets to the higher protocol layers. */ int ip_local_deliver(struct sk_buff *skb) { /* * Reassemble IP fragments. */ struct net *net = dev_net(skb->dev); //check if it is a fragment if (ip_is_fragment(ip_hdr(skb))) { //fragment recombination if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; } return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, net, NULL, skb, skb->dev, NULL, ip_local_deliver_finish); }
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb) { __skb_pull(skb, skb_network_header_len(skb)); rcu_read_lock(); { //get the protocol of this packet int protocol = ip_hdr(skb)->protocol; const struct net_protocol *ipprot; ..... //from inet_protos list to get the correct protocol struct depending on protocol as index ipprot = rcu_dereference(inet_protos[protocol]); if(ipprot) { ... ret = ipprot->handler(skb);//call the Lay4 handler function. ... } } .... }
從上可知,在ip層處理最后,會從skb(socket buffer)中得到協議,然后調用對應的協議(TCP或UDP)處理器。如下:
實際中大多數使用recvfrom,而非recv或recvmsg,那它們的區別是什么呢,參見http://man7.org/linux/man-pages/man2/recvfrom.2.html。
參見:https://segmentfault.com/a/1190000008836467
https://blog.csdn.net/Charce1989/article/details/70766955
http://www.ece.virginia.edu/mv/research/DOE09/publications/TCPlinux.pdf
UNIX網絡編程 卷1 套接字聯網API 第3版
