Linux網路 - 資料包的接收過程
轉自https://segmentfault.com/a/1190000008836467
本文將介紹在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_FOWARD |
| | +------------+ +----------------+
+---------+ |
| |
| 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。
參考
Monitoring and Tuning the Linux Networking Stack: Receiving Data
Illustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
NAPI