linux網絡報文接收發送淺析

對於linux內核來說,網絡報文由網絡設備來進行接收。設備驅動程序從網絡設備中讀取報文,通過內核提供的網絡接口函數,將報文傳遞到內核中的網絡協議棧。報文經過協議棧的處理,或轉發、或丟棄、或被傳送給某個進程。

  網絡報文的發送與之相反,進程通過系統調用將數據送入網絡協議棧,或者由網絡協議棧自己發起報文的發送,然後協議棧通過調用網絡接口函數來調度驅動程序,使其將報文傳送給網絡設備,從而發送出去。

  本文討論的是網絡接口層,它是網絡設備驅動程序與網絡協議棧交互的紐帶。見下圖中紅色部分的netif

  報文的接收

  網絡報文的接收源自網絡設備。網絡設備在接收到一個報文之後,通過中斷告知CPU。網卡驅動程序需要註冊對該中斷事件的處理函數(參見《linux中斷處理淺析》),以處理接收到的報文。

  在中斷處理函數中,網絡驅動程序有兩種方法對報文進行處理(老式的方法,和新式的方法),我們先介紹老式的處理方式。在這種方式下,中斷處理函數主要完成以下工作:

  分配一個skb結構(該結構用於保存一個報文)。操作設備,將設備收到的數據拷貝到這個skb結構對應的緩衝區中。設置skb的協議類型skb->protocol,該類型表明了網絡協議棧的上層協議(下面我們將會看到)。然後調用內核提供的網絡接口函數netif_rx;

  netif_rx(skb);

  netif_rx函數對skb的如時間戳這樣的附加信息進行初始化以後,將這個skb結構放入當前CPU的softdate_net結構的input_pkt_queue隊列中。netif_rx會根據隊列的長度,對設備的擁塞狀況進行判斷(隊列過長則代表報文接收過快,以致於上層來不及處理)。如果設備已陷入擁塞,則收到的報文可能直接被丟棄。

  如果一切正常,netif_rx會調用網絡接口函數netif_rx_schedule,以觸發對接收報文的進一步處理;

  netif_rx_schedule(dev);

  netif_rx使用softdate_net結構中內嵌的backlog_dev作爲dev來調用netif_rx_schedule,後者將其加入到softdate_net結構的poll_list隊列中(如果這個dev不在隊列中的話),以使其等待被調度。

  相比老式的處理方式,新式的處理方式(稱爲NAPI)在中斷處理函數中僅僅是以對應設備的dev結構爲參數調用netif_rx_schedule函數即可。

  最後netif_rx_schedule函數會觸發NET_RX_SOFTIRQ軟中斷,於是接下來對應的軟中斷處理函數net_rx_action將被調用;

  net_rx_action();

  對於當前CPU對應的softdate_net結構的poll_list隊列中的所有dev,調用dev->poll方法。該方法是由對應dev的驅動程序實現的,用於接收及處理報文(前面提到的backlog_dev除外)。

  net_rx_action每次運行都有一定的限度,並不一定要將所有報文都處理完。在處理完一定數量的報文配額、或處理過程超過一定時間後,net_rx_action便會返回。返回前觸發一次NET_RX_SOFTIRQ軟中斷,等待下一次中斷到來的時候繼續被調度。

  以上過程如圖所示(摘自ULNI):

  上面提到的softdate_net結構是用於進行報文收發調度的結構,內核爲每個CPU維護一個這樣的結構。在報文接收過程中用到了其中的三個成員:

  1、poll_list,網絡設備dev的隊列。其中的設備接收到了報文,需要被處理;

  2、input_pkt_queue,skb報文結構的隊列,保存了已接收並需要被處理的報文;

  3、backlog_dev,一個虛擬的網絡設備dev結構;

  後兩個成員是專門爲支持老式的處理方式而設置的,在這種方式下,接收到的skb被放入input_pkt_queue隊列,然後backlog_dev被加入poll_list。而最後,自然backlog_dev->poll函數將對input_pkt_queue隊列中的skb進行處理。backlog_dev->poll等於process_backlog函數;

  process_backlog(backlog_dev,budget);

  既然net_rx_action每次運行都有一個配額,它在調用dev->poll時也會傳遞當前剩餘的配額值,即budget。

  process_backlog會遍歷input_pkt_queue隊列中的skb,調用netif_receive_skb函數對其進行處理。

  process_backlog函數有兩種結局,一個是配額到或時間到,直接返回;另一個是處理完input_pkt_queue隊列中的所有skb,此時需要將backlog_dev從poll_list中刪除。

  新式的NAPI處理方式所要做的事跟老的處理方式其實是很類似的。在其對應的dev->poll函數中,需要分配skb結構、從設備讀取報文、調用netif_receive_skb讓網絡協議棧的上層來處理報文。

  這種方式最大的好處是:在dev->poll函數中,不一定只處理一個報文。具體怎麼處理可以由驅動程序靈活控制。比如說,假設現在網絡負載非常大,如果網絡設備每接收一個報文都通過一次中斷來告知內核,這樣做效率並不理想。而此時dev->poll可以做一些輪詢的工作,如果網絡設備已經接收了多個報文,可以一次性都處理了。並且,就算設備此刻所接收到的報文都已經處理完了,驅動程序也可以根據某種方式預判設備在很短的一段時間內還將收到報文,於是依然將自己對應的dev結構留在poll_list中,等待下一次繼續被調度。

  當dev仍結構留在poll_list中時,設備驅動程序可以關閉設備接收到報文時的中斷通知,因爲目前處於輪詢狀態。而當驅動程序認爲在將來的一段時間以內無報文可收時,則可以將其dev從poll_list中移除,然後開啓設備接收到報文時的中斷通知。等待下一次報文接收的中斷到來時,這個dev再重新被放入poll_list。

  netif_receive_skb(skb);

  該函數會將skb提交給抓包程序進行處理、還會觸發數據鏈路層的橋接功能(見《linux網橋淺析》)、然後將報文提交給網絡協議棧的上層(網絡層)進行處理。

  網絡層的協議有IP、ARP等等很多種,在這裏怎麼知道這個skb該提交給哪種協議呢?在報文的數據鏈路層報頭中保存着三個重要信息,發送者和接收者的Mac地址、和上層協議標識。回想一下之前的流程,在skb接收完成之後我們就已經設置了skb->protocol(從報頭中得到),上層協議就由它來指定。比如,0x0800代表IP協議、0x0806代表ARP協議,這是由協議規定的。

netif_receive_skb並不是用一個switch-case來匹配skb->protocol,以選擇網絡層處理函數的。系統中有一個名爲ptype_base的hash表,各種網絡層的協議在其初始化時都會在這個hash表中註冊一個類型爲packet_type的表項(以協議類型爲key),如下圖所示(摘自ULNI):

  netif_receive_skb要做的就是在這個hash表中遍歷所有type與skb->protocol匹配的packet_type結構(packet_type結構的dev可用於限定skb->dev,NULL表示不限),然後調用其func回調函數。(可見,一個報文有可能被多種協議所處理。)

  至此報文被提交到了網絡層,在這裏就不繼續深入了。

  報文的發送

  報文的發送是由網絡協議棧的上層發起的。網絡協議棧上層構造一個需要發送的skb結構後(該skb已經包含了數據鏈路層的報頭),調用dev_queue_xmit函數進行發送;

  dev_queue_xmit(skb);

  該函數先會處理一些緩衝區重組、計算校驗和之類的雜事,然後開始處理報文的發送。

  發送報文有兩種策略,有隊列或無隊列。這是由網絡設備驅動程序在定義其對應的dev結構時指定的,一般的設備都會使用隊列。

  dev->qdisc指向一個隊列的實例,裏面包含了隊列本身以及操作隊列的方法(enqueue、dequeue、requeue)。這些方法的集合組成了一種隊列規則(skb將以某種規則入隊、以某種規則出隊,並不一定是簡單的先進先出),這樣的規則可用於流量控制。

  網絡設備驅動程序可以選擇自己的設備使用什麼樣的隊列,或是不使用隊列。

  對於有隊列的設備,dev_queue_xmit調用dev->qdisc->enqueue方法將skb加入隊列,然後調用qdisc_run函數。而qdisc_run會調用qdisc_restart來對隊列進行處理。

  qdisc_restart(dev);

  該函數主要的工作就是不斷調用dev->qdisc->dequeue方法從隊列中取出待發送的報文,然後調用dev->hard_start_xmit方法進行發送。該方法是由設備驅動程序實現的,會直接和網絡設備去打交道,將報文發送出去。

  如果報文發送失敗,qdisc_restart會調用dev->qdisc->requeue方法將skb重新放回隊列。同時,還將調用netif_schedule函數將dev加入softdate_net的output_queue隊列中(其中的設備都是有報文等待發送的,將在稍後被處理)。然後觸發一次NET_TX_SOFTIRQ軟中斷。於是在下一個中斷到來時,對應的軟中斷處理函數net_tx_action將被調用。

  而如果dev->hard_start_xmit方法發送報文成功,則表示報文已經送到了網絡設備的發送緩衝區,設備會自動將報文發送出去。並且在報文發送完成時,設備會通過中斷通知驅動程序。對應的中斷處理函數也會觸發NET_TX_SOFTIRQ軟中斷。此外,已發送完成的skb將被加入softdate_net的completion_queue隊列中,等待被釋放。

  軟中斷NET_TX_SOFTIRQ被觸發,將使得net_tx_action函數被調用。該函數主要做了兩件事:

  1、從softdate_net的completion_queue隊列中取出每一個skb,將其釋放;

  2、對於softdate_net的output_queue隊列中的dev,調用qdisc_run繼續嘗試發送其qdisc隊列中的報文;

  對於有隊列的設備,其隊列主要用於流量控制以及發送失敗時的緩衝;對於沒有隊列的設備(比如lo,環回設備),dev_queue_xmit函數則會直接調用dev->hard_start_xmit進行發送,如果失敗報文就會被丟棄。

  以上過程如圖所示:

  qdisc_restart函數在執行過程中還會關心dev是否被暫停(就如接收報文時要關心網絡是否擁塞一樣),如果被暫停則結束處理流程並返回。

  而dev的暫停與否是由設備驅動程序來設置的,在dev->hard_start_xmit函數中,驅動程序如果發現設備當前的發送緩衝區太小(比如小到無法再容納一個報文。這表示報文發送過快,以致於設備來不及處理),則會讓設備暫停。而當網絡設備在完成報文的發送後會產生中斷,對應的中斷處理程序又可以根據設備當前的發送緩衝區大小,決定是否讓設備從暫停中恢復。

  而如果網絡設備出現問題,無法發送報文了,則可能設備上的發送緩衝區一直處於被佔滿的狀態,導致設備一直被暫停。另一方面,報文發不出去,也就不會有通知發送完成的中斷產生,設備也就不會從暫停狀態恢復,於是網絡就癱瘓了。

  爲了檢測這種情況,驅動程序可以爲設備設置一個看門狗定時器。如果發現設備正在暫停狀態,並且距離最後一次發送報文已經過去一定的時間,而發送完成的中斷還沒有收到,則認爲該設備出現問題。此時看門狗定時器將觸發驅動程序提供的相關函數,將設備復位,以試圖讓其恢復正常工作。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章