轉自:https://segmentfault.com/a/1190000008836467?utm_source=tag-newest
本文將介紹在Linux系統中,數據包是如何一步一步從網卡傳到進程手中的。
如果英文沒有問題,強烈建議閱讀后面參考里的兩篇文章,里面介紹的更詳細。
本文只討論以太網的物理網卡,不涉及虛擬設備,並且以一個UDP包的接收過程作為示例.
本示例里列出的函數調用關系來自於kernel 3.13.0,如果你的內核不是這個版本,函數名稱和相關路徑可能不一樣,但背后的原理應該是一樣的(或者有細微差別)
網卡到內存
網卡需要有驅動才能工作,驅動是加載到內核中的模塊,負責銜接網卡和內核的網絡模塊,驅動在加載的時候將自己注冊進網絡模塊,當相應的網卡收到數據包時,網絡模塊會調用相應的驅動程序處理數據。
下圖展示了數據包(packet)如何進入內存,並被內核的網絡模塊開始處理:
+-----+
| | Memroy
+--------+ 1 | | 2 DMA +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+ | | +--------+--------+--------+--------+
| |<--------+
+-----+ |
| +---------------+
| |
3 | Raise IRQ | Disable IRQ
| 5 |
| |
↓ |
+-----+ +------------+
| | Run IRQ handler | |
| CPU |------------------>| NIC Driver |
| | 4 | |
+-----+ +------------+
|
6 | Raise soft IRQ
|
↓
1: 數據包從外面的網絡進入物理網卡。如果目的地址不是該網卡,且該網卡沒有開啟混雜模式,該包會被網卡丟棄。
2: 網卡將數據包通過DMA的方式寫入到指定的內存地址,該地址由網卡驅動分配並初始化。注: 老的網卡可能不支持DMA,不過新的網卡一般都支持。
3: 網卡通過硬件中斷(IRQ)通知CPU,告訴它有數據來了
4: CPU根據中斷表,調用已經注冊的中斷函數,這個中斷函數會調到驅動程序(NIC Driver)中相應的函數
5: 驅動先禁用網卡的中斷,表示驅動程序已經知道內存中有數據了,告訴網卡下次再收到數據包直接寫內存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
6: 啟動軟中斷。這步結束后,硬件中斷處理函數就結束返回了。由於硬中斷處理程序執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬件的中斷,於是內核引入軟中斷,這樣可以將硬中斷處理函數中耗時的部分移到軟中斷處理函數里面來慢慢處理。
內核的網絡模塊
軟中斷會觸發內核網絡模塊中的軟中斷處理函數,后續流程如下
+-----+
17 | |
+----------->| NIC |
| | |
|Enable IRQ +-----+
|
|
+------------+ Memroy
| | Read +--------+--------+--------+--------+
+--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
| | | 9 +--------+--------+--------+--------+
| +------------+
| | | skb
Poll | 8 Raise softIRQ | 6 +-----------------+
| | 10 |
| ↓ ↓
+---------------+ Call +-----------+ +------------------+ +--------------------+ 12 +---------------------+
| net_rx_action |<-------| ksoftirqd | | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
+---------------+ 7 +-----------+ +------------------+ 11 +--------------------+ +---------------------+
| | 13
14 | + - - - - - - - - - - - - - - - - - - - - - - +
↓ ↓
+--------------------------+ 15 +------------------------+
| __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
+--------------------------+ +------------------------+
|
| 16
↓
+-----------------+
| protocol layers |
+-----------------+
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: 調用協議棧相應的函數,將數據包交給協議棧處理。
17: 待內存中的所有數據包被處理完成后(即poll函數執行完成),啟用網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知CPU
enqueue_to_backlog函數也會被netif_rx函數調用,而netif_rx正是lo設備發送數據包時調用的函數
協議棧
IP層
由於是UDP包,所以第一步會進入IP層,然后一級一級的函數往下調:
|
|
↓ promiscuous mode &&
+--------+ PACKET_OTHERHOST (set by driver) +-----------------+
| ip_rcv |-------------------------------------->| drop this packet|
+--------+ +-----------------+
|
|
↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
|
|
↓
+---------+
| | enabled ip forword +------------+ +----------------+
| routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
| | +------------+ +----------------+
+---------+ |
| |
| destination IP is local ↓
↓ +---------------+
+------------------+ | dst_output_sk |
| ip_local_deliver | +---------------+
+------------------+
|
|
↓
+------------------+
| NF_INET_LOCAL_IN |
+------------------+
|
|
↓
+-----------+
| UDP layer |
+-----------+
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,那么將會調用該函數,在該函數中,會先調用NF_INET_LOCAL_IN相關的鈎子程序,如果通過,數據包將會向下發送到UDP層
UDP層
|
|
↓
+---------+ +-----------------------+
| udp_rcv |----------->| __udp4_lib_lookup_skb |
+---------+ +-----------------------+
|
|
↓
+--------------------+ +-----------+
| sock_queue_rcv_skb |----->| sk_filter |
+--------------------+ +-----------+
|
|
↓
+------------------+
| __skb_queue_tail |
+------------------+
|
|
↓
+---------------+
| sk_data_ready |
+---------------+
udp_rcv: udp_rcv函數是UDP模塊的入口函數,它里面會調用其它的函數,主要是做一些必要的檢查,其中一個重要的調用是__udp4_lib_lookup_skb,該函數會根據目的IP和端口找對應的socket,如果沒有找到相應的socket,那么該數據包將會被丟棄,否則繼續
sock_queue_rcv_skb: 主要干了兩件事,一是檢查這個socket的receive buffer是不是滿了,如果滿了的話,丟棄該數據包,然后就是調用sk_filter看這個包是否是滿足條件的包,如果當前socket上設置了filter,且該包不滿足條件的話,這個數據包也將被丟棄(在Linux里面,每個socket上都可以像tcpdump里面一樣定義filter,不滿足條件的數據包將會被丟棄)
__skb_queue_tail: 將數據包放入socket接收隊列的末尾
sk_data_ready: 通知socket數據包已經准備好
調用完sk_data_ready之后,一個數據包處理完成,等待應用層程序來讀取,上面所有函數的執行過程都在軟中斷的上下文中。
socket
應用層一般有兩種方式接收數據,一種是recvfrom函數阻塞在那里等着數據來,這種情況下當socket收到通知后,recvfrom就會被喚醒,然后讀取接收隊列的數據;另一種是通過epoll或者select監聽相應的socket,當收到通知后,再調用recvfrom函數去讀取接收隊列的數據。兩種情況都能正常的接收到相應的數據包。
結束語
了解數據包的接收流程有助於幫助我們搞清楚我們可以在哪些地方監控和修改數據包,哪些情況下數據包可能被丟棄,為我們處理網絡問題提供了一些參考,同時了解netfilter中相應鈎子的位置,對於了解iptables的用法有一定的幫助,同時也會幫助我們后續更好的理解Linux下的網絡虛擬設備。
在接下來的幾篇文章中,將會介紹Linux下的網絡虛擬設備和iptables。