1. 接收數據包過程概述
介紹數據包收包過程,有助于我們了解Linux內核網絡設備在數據收包過程中的位置,下面從宏觀的角度介紹數據包從被網卡接收到進入 socket 接收隊列的整個過程:
加載網卡驅動,初始化
數據包從外部網絡進入網卡
網卡(通過DMA)將包拷貝到內核內存中的ring buffer
產生硬件中斷,通知系統收到了一個包
驅動調用 NAPI ,如果輪詢(poll)還沒有開始,就開始輪詢
ksoftirqd軟中斷調用 NAPI 的poll函數從ring buffer收包(poll 函數是網卡驅動在初始化階段注冊的;每個cpu上都運行著一個ksoftirqd進程,在系統啟動期間就注冊了)
ring buffer里面對應的內存區域解除映射(unmapped)
如果 packet steering 功能打開,或者網卡有多隊列,網卡收到的數據包會被分發到多個cpu
數據包從隊列進入協議層
協議層處理數據包
數據包從協議層進入相應 socket 的接收隊列
下面以常見的Intel I350 網卡的驅動 ibg 為例介紹它的工作過程:
驅動會使用module_init向內核注冊一個初始化函數,當驅動被加載時,內核會調用這個函數。在drivers/net/ethernet/intel/igb/igb_main.c中初始化函數(igb_init_module):
/*** igb_init_module - Driver Registration Routine** igb_init_module is the first routine called when the driver is* loaded. All it does is register with the PCI subsystem.**/static int __init igb_init_module(void){int ret;pr_info("%s - version %s\n", igb_driver_string, igb_driver_version);pr_info("%s\n", igb_copyright);/* ... */ret = pci_register_driver(&igb_driver);return ret;}module_init(igb_init_module);
初始化的大部分工作在pci_register_driver中完成。
Intel I350 網卡是 PCI express 設備。PCI 設備通過PCI Configuration Space 里面的寄存器識別自己。
PCI express 總線是一種完全不同于過去PCI總線的一種全新總線規范,與PCI總線共享并行架構相比,PCI Express總線是一種點對點串行連接的設備連接方式,點對點意味著每一個PCI Express設備都擁有自己獨立的數據連接,各個設備之間并發的數據傳輸互不影響,而對于過去PCI那種共享總線方式,PCI總線上只能有一個設備進行通信,一旦PCI總線上掛接的設備增多,每個設備的實際傳輸速率就會下降,性能得不到保證。PCI Express以點對點的方式處理通信,每個設備在要求傳輸數據的時候各自建立自己的傳輸通道,對于其他設備這個通道是封閉的,這樣的操作保證了通道的專有性,避免其他設備的干擾。
當設備驅動編譯時,MODULE_DEVICE_TABLE 宏(定義在 include/module.h) 會導出一個 PCI 設備 ID 列表(a table of PCI device IDs),驅動據此識別它可以控制的設備,內核也會依據這個列表對不同設備加載相應驅動。
igb 驅動的設備表和 PCI 設備 ID 分別見:drivers/net/ethernet/intel/igb/igb_main.c 和drivers/net/ethernet/intel/igb/e1000_hw.h。
static DEFINE_PCI_DEVICE_TABLE(igb_pci_tbl) = {{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_1GBPS) },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_SGMII) },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I354_BACKPLANE_2_5GBPS) },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I211_COPPER), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_FIBER), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SGMII), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_COPPER_FLASHLESS), board_82575 },{ PCI_VDEVICE(INTEL, E1000_DEV_ID_I210_SERDES_FLASHLESS), board_82575 },/* ... */};MODULE_DEVICE_TABLE(pci, igb_pci_tbl);
前面提到,驅動初始化的時候會調用 pci_register_driver,這個函數會將該驅動的各種回調方法注冊到一個 struct pci_driver 變量,drivers/net/ethernet/intel/igb/igb_main.c:
static struct pci_driver igb_driver = {.name = igb_driver_name,.id_table = igb_pci_tbl,.probe = igb_probe,.remove = igb_remove,/* ... */};
通過 PCI ID 識別設備后,內核就會為它選擇合適的驅動。每個 PCI 驅動注冊了一個 probe() 方法,內核會對每個設備依次調用其驅動的 probe 方法,一旦找到一個合適的驅動,就不會再為這個設備嘗試其他驅動。
很多驅動都需要大量代碼來使得設備 ready,具體做的事情各有差異。典型的過程:
啟用 PCI 設備
請求(requesting)內存范圍和 IO 端口
設置 DMA 掩碼
注冊設備驅動支持的 ethtool 方法(后面介紹)
注冊所需的 watchdog(例如,e1000e 有一個檢測設備是否僵死的 watchdog)
其他和具體設備相關的事情,例如一些 workaround,或者特定硬件的非常規處理
創建、初始化和注冊一個 struct net_device_ops 類型變量,這個變量包含了用于設備相關的回調函數,例如打開設備、發送數據到網絡、設置 MAC 地址等
創建、初始化和注冊一個更高層的 struct net_device 類型變量(一個變量就代表了 一個設備)
下面來看 igb 驅動的 igb_probe 包含哪些過程(drivers/net/ethernet/intel/igb/igb_main.c):
err = pci_enable_device_mem(pdev);/* ... */err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));/* ... */err = pci_request_selected_regions(pdev, pci_select_bars(pdev,IORESOURCE_MEM),igb_driver_name);pci_enable_pcie_error_reporting(pdev);pci_set_master(pdev);pci_save_state(pdev);
更詳細的過程可以查看內核文檔:https://github.com/torvalds/linux/blob/v3.13/Documentation/PCI/pci.txt
igb_probe 做了很多重要的設備初始化工作。除了 PCI 相關的,還有如下一些通用網絡功能和網絡設備相關的工作:
注冊 struct net_device_ops 變量
注冊 ethtool 相關的方法
從網卡獲取默認 MAC 地址
設置 net_device 特性標記
網絡設備相關的操作函數都注冊到struct net_device_ops類型的變量中(drivers/net/ethernet/intel/igb/igb_main.c):
static const struct net_device_ops igb_netdev_ops = {.ndo_open = igb_open,.ndo_stop = igb_close,.ndo_start_xmit = igb_xmit_frame,.ndo_get_stats64 = igb_get_stats64,.ndo_set_rx_mode = igb_set_rx_mode,.ndo_set_mac_address = igb_set_mac,.ndo_change_mtu = igb_change_mtu,.ndo_do_ioctl = igb_ioctl,/* ... */
這個變量會在igb_probe()中賦給struct net_device中的netdev_ops字段:
static int igb_probe(struct pci_dev *pdev, const struct pci_device_id *ent){...netdev->netdev_ops = &igb_netdev_ops;}
ethtool 是一個命令行工具,可以查看和修改網絡設備的一些配置,常用于收集網卡統計數據。在 Ubuntu 上,可以 通過 apt-get install ethtool 安裝,過會演示通過此工具監控網卡數據。
ethtool 通過 ioctl 和設備驅動通信。內核實現了一個通用 ethtool 接口,網卡驅動實現這些接口,就可以被 ethtool 調用。當 ethtool 發起一個系統調用之后,內核會找到對應操作的回調函數 。回調實現了各種簡單或復雜的函數,簡單的如改變一個 flag 值,復雜的包括調整網卡硬件如何運行。
相關實現見:drivers/net/ethernet/intel/igb/igb_ethtool.c。
當一個數據幀通過 DMA 寫到 RAM(內存)后,網卡是如何通知其他系統這個包可以被處理了呢?
傳統的方式是,網卡會產生一個硬件中斷(IRQ),通知數據包到了。有三種常見的硬中斷類型:
MSI-X
MSI
legacy IRQ
如果有大量的數據包到達,就會產生大量的硬件中斷。CPU 忙于處理硬件中斷的時候,可用于處理其他任務的時間就會減少。
NAPI(New API)是一種新的機制,可以減少產生的硬件中斷的數量(但不能完全消除硬中斷 )。
NAPI 接收數據包的方式和傳統方式不同,它允許設備驅動注冊一個 poll 方法,然后調用這個方法完成收包。
NAPI 的使用方式:
驅動打開 NAPI 功能,默認處于未工作狀態(沒有在收包)
數據包到達,網卡通過 DMA 寫到內存
網卡觸發一個硬中斷,中斷處理函數開始執行
軟中斷(softirq),喚醒 NAPI 子系統。這會觸發在一個單獨的線程里, 調用驅動注冊的 poll 方法收包
驅動禁止網卡產生新的硬件中斷,這樣做是為了 NAPI 能夠在收包的時候不會被新的中斷打擾
一旦沒有包需要收了,NAPI 關閉,網卡的硬中斷重新開啟
轉步驟 2
和傳統方式相比,NAPI 一次中斷會接收多個包,因此可以減少硬件中斷的數量。
poll 方法是通過調用 netif_napi_add 注冊到 NAPI 的,同時還可以指定權重 weight,大部分驅動都 hardcode 為 64。
通常來說,驅動在初始化的時候注冊 NAPI poll 方法。
igb 驅動的初始化過程是一個很長的調用鏈:
igb_probe -> igb_sw_init
igb_sw_init -> igb_init_interrupt_scheme
igb_init_interrupt_scheme -> igb_alloc_q_vectors
igb_alloc_q_vectors -> igb_alloc_q_vector
igb_alloc_q_vector -> netif_napi_add
從宏觀角度來看,這個調用過程會做以下事情:
如果支持 MSI-X,調用 pci_enable_msix 打開它
計算和初始化一些配置,包括網卡收發隊列的數量
調用 igb_alloc_q_vector 創建每個發送和接收隊列
igb_alloc_q_vector 會進一步調用 netif_napi_add 注冊 poll 方法到 NAPI 變量
下面介紹 igb_alloc_q_vector 是如何注冊 poll 方法和私有數據的(drivers/net/ethernet/intel/igb/igb_main.c):
static int igb_alloc_q_vector(struct igb_adapter *adapter,int v_count, int v_idx,int txr_count, int txr_idx,int rxr_count, int rxr_idx){/* ... *//* allocate q_vector and rings */q_vector = kzalloc(size, GFP_KERNEL);if (!q_vector)return -ENOMEM;/* initialize NAPI */netif_napi_add(adapter->netdev, &q_vector->napi, igb_poll, 64);/* ... */
q_vector 是新分配的隊列,igb_poll 是 poll 方法,當它收包的時候,會通過這個接收隊列找到關聯的 NAPI 變量(q_vector->napi)。
前面提到structure net_device_ops 變量,它包含網卡啟用、發包、設置 mac 地址等回調函數(函數指針)。
當啟用一個網卡時(例如,通過 ifconfig eth0 up),net_device_ops 的 ndo_open 方法會被調用。它通常會做以下事情:
分配 RX、TX 隊列內存
打開 NAPI 功能
注冊中斷處理函數
打開(enable)硬中斷
其他
igb 驅動中,這個方法對應的是 igb_open 函數。
目前大部分網卡都使用 DMA 將數據直接寫到內存,接下來操作系統可以直接從里面讀取。實現這一目的所使用的數據結構是 ring buffer(環形緩沖區)。
要實現這一功能,設備驅動必須和操作系統合作,預留(reserve)出一段內存來給網卡使用。預留成功后,網卡知道了這塊內存的地址,接下來收到的數據包就會放到這里,進而被操作系統取走。
由于這塊內存區域是有限的,如果數據包的速率非常快,單個 CPU 來不及取走這些包,新來的包就會被丟棄。這時候,Receive Side Scaling(RSS,接收端擴展)或者多隊列( multiqueue)一類的技術可能就會排上用場。
一些網卡有能力將接收到的數據包寫到多個不同的內存區域,每個區域都是獨立的接收隊列。這樣操作系統就可以利用多個 CPU(硬件層面)并行處理收到的數據包。只有部分網卡支持這個功能。
Intel I350 網卡支持多隊列,我們可以在 igb 的驅動里看出來。igb 驅動啟用的時候 ,最開始做的事情之一就是調用 igb_setup_all_rx_resources 函數。這個函數會對每個 RX 隊列調用 igb_setup_rx_resources, 里面會管理 DMA 的內存。
RX 隊列的數量和大小可以通過 ethtool 進行配置,調整這兩個參數會對收包或者丟包產生可見影響。
網卡通過對 packet 頭(例如源地址、目的地址、端口等)做哈希來決定將 packet 放到哪個 RX 隊列。只有很少的網卡支持調整哈希算法。如果支持的話,可以根據算法將特定 的 flow 發到特定的隊列,甚至可以做到在硬件層面直接將某些包丟棄。
一些網卡支持調整 RX 隊列的權重,可以有意地將更多的流量發到指定的 queue。
前面介紹了驅動如何注冊 NAPI poll 方法,但是,一般直到網卡被啟用之后,NAPI 才被啟用。
啟用 NAPI 很簡單,調用 napi_enable 函數就行,這個函數會設置 NAPI 變量(struct napi_struct)中一個表示是否啟用的標志位。前面說到,NAPI 啟用后并不是立即開始工作(而是等硬中斷觸發)。
對于 igb,驅動初始化或者通過 ethtool 修改 queue 數量或大小的時候,會啟用每個 q_vector 的 NAPI 變量( drivers/net/ethernet/intel/igb/igb_main.c):
for (i = 0; i < adapter->num_q_vectors; i++)napi_enable(&(adapter->q_vector[i]->napi));
啟用 NAPI 之后,下一步就是注冊中斷處理函數。設備有多種方式觸發一個中斷:
MSI-X
MSI
legacy interrupts
設備驅動的實現也因此而異。驅動必須判斷出設備支持哪種中斷方式,然后注冊相應的中斷處理函數,這些函數在中斷發生的時候會被執行。
一些驅動,例如 igb,會試圖為每種中斷類型注冊一個中斷處理函數,如果注冊失敗,就嘗試下一種類型。
MSI-X 中斷是比較推薦的方式,尤其是對于支持多隊列的網卡。因為每個 RX 隊列有獨立的 MSI-X 中斷,因此可以被不同的 CPU 處理(通過 irqbalance 方式,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity)。處理中斷的 CPU 也是隨后處理這個包的 CPU。這樣的話,從網卡硬件中斷的層面就可以設置讓收到的包被不同的 CPU 處理。
如果不支持 MSI-X,那 MSI 相比于傳統中斷方式仍然有一些優勢,驅動仍然會優先考慮它。
在 igb 驅動中,函數 igb_msix_ring,igb_intr_msi,igb_intr 分別是 MSI-X,MSI 和傳統中斷方式的中斷處理函數。
驅動是如何嘗試各種中斷類型的( drivers/net/ethernet/intel/igb/igb_main.c):
static int igb_request_irq(struct igb_adapter *adapter){struct net_device *netdev = adapter->netdev;struct pci_dev *pdev = adapter->pdev;int err = 0;if (adapter->msix_entries) {err = igb_request_msix(adapter);if (!err)goto request_done;/* fall back to MSI *//* ... */}/* ... */if (adapter->flags & IGB_FLAG_HAS_MSI) {err = request_irq(pdev->irq, igb_intr_msi, 0,netdev->name, adapter);if (!err)goto request_done;/* fall back to legacy interrupts *//* ... */}err = request_irq(pdev->irq, igb_intr, IRQF_SHARED,netdev->name, adapter);if (err)dev_err(&pdev->dev, "Error %d getting interrupt\n", err);request_done:return err;}
這就是 igb 驅動注冊中斷處理函數的過程,這個函數在一個數據包到達網卡觸發一個硬件中斷時就會被執行。
到這里,幾乎所有的準備工作都就緒了。唯一剩下的就是打開硬中斷,等待數據包進來。打開硬中斷的方式因硬件而異,igb 驅動是在 __igb_open 里調用輔助函數 igb_irq_enable 完成的。
中斷通過寫寄存器的方式打開:
static void igb_irq_enable(struct igb_adapter *adapter){/* ... */wr32(E1000_IMS, IMS_ENABLE_MASK | E1000_IMS_DRSTA);wr32(E1000_IAM, IMS_ENABLE_MASK | E1000_IMS_DRSTA);/* ... */}
現在,網卡已經啟用了。驅動可能還會做一些額外的事情,例如啟動定時器,工作隊列( work queue),或者其他硬件相關的設置。這些工作做完后,網卡就可以接收數據包了。
監控網絡設備有幾種不同的方式,每種方式的監控粒度(granularity)和復雜度不同。我們先從最粗的粒度開始,逐步細化。
ethtool -Sethtool -S 可以查看網卡統計信息(例如接收和發送的數據包總數,接收和發送的流量,丟棄的包數量,錯誤的數據包數量等):

監控這些數據比較困難。因為用命令行獲取很容易,但是以上字段并沒有一個統一的標準。不同的驅動,甚至同一驅動的不同版本可能字段都會有差異。
可以先粗略的查看 “drop”, “buffer”, “miss” 等字樣。然后,在驅動的源碼里找到對應的更新這些字段的地方,這可能是在軟件層面更新的,也有可能是在硬件層面通過寄存器更新的。如果是通過硬件寄存器的方式,就得查看網卡的 data sheet(說明書),搞清楚這個寄存器代表什么。ethtoool 給出的這些字段名,有一些是有誤導性的(misleading)。
sysfssysfs 也提供了統計信息,但相比于網卡層的統計,要更上層一些。
例如,可以獲取的 ens33 的接收端數據包的類型有這些:

獲取接收到的數據包的總數為:

不同類型的統計分別位于 /sys/class/net/ 下面的不同文件,包括 collisions, rx_dropped, rx_errors, rx_missed_errors 等等。
要注意的是,每種類型代表什么意思,是由驅動來決定的,因此也是由驅動決定何時以及在哪里更新這些計數的。你可能會發現一些驅動將一些特定類型的錯誤歸類為 drop,而另外一些驅動可能將它們歸類為 miss。
這些值至關重要,因此需要查看對應的網卡驅動,搞清楚它們真正代表什么。
/proc/net/dev/proc/net/dev 提供了更高一層的網卡統計。

這個文件里顯示的統計只是 sysfs 里面的一個子集,但適合作為一個常規的統計參考。
如果對這些數據準確度要求特別高,那必須查看內核源碼 、驅動源碼和驅動手冊,搞清楚每個字段真正代表什么意思,計數是如何以及何時被更新的。Linux內核網絡設備驅動先介紹到這里,感謝閱讀。
參考鏈接:
https://blog.packagecloud.io/eng/2016/06/22/monitoring-tuning-linux-networking-stack-receiving-data/
end
一口Linux?
關注,回復【1024】海量Linux資料贈送
精彩文章合集
文章推薦
點擊“閱讀原文”查看更多分享,歡迎點分享、收藏、點贊、在看