天天看點

網卡接收資料流程

作者:後端開發進階

網卡(Network Interface Card,簡稱NIC),也稱網絡擴充卡,是電腦與區域網路互相連接配接的裝置。

網卡的組成

網卡工作在實體層和資料鍊路層,主要由 PHY/MAC 晶片、Tx/Rx FIFO、DMA 等組成,其中網線通過變壓器接 PHY 晶片、PHY 晶片通過 MII 接 MAC 晶片、MAC 晶片接 PCI 總線。

PHY 晶片主要負責:CSMA/CD、模數轉換、編解碼、串并轉換。

MAC 晶片主要負責:

比特流和資料幀的轉換(7 位元組的前導碼 Preamble 和 1 位元組的幀首定界符 SFD);

CRC 校驗;

Packet Filtering(L2 Filtering、VLAN Filtering、Manageability/Host Filtering)。

Tx/Rx FIFO:Tx 表示發送(Transport),Rx 是接收(Receive)。

DMA(Direct Memory Access):直接存儲器存取 I/O 子產品。

CPU 與網卡的協同

以往,從網卡的 I/O 區域,包括 I/O 寄存器或 I/O 記憶體中讀取資料,這都要 CPU 親自去讀,然後把資料放到 RAM 中,也就占用了 CPU 的運算資源。直到出現了 DMA 技術,其基本思想是外設和 RAM 之間開辟直接的資料傳輸通路。一般情況下,總線所有的工作周期(總線周期)都用于 CPU 執行程式。DMA 控制就是當外設完成資料 I/O 的準備工作之後,會占用總線的一個工作周期,和 RAM 直接交換資料。這個周期之後,CPU 又繼續控制總線執行原程式。如此反複的,直到整個資料塊的資料全部傳輸完畢,進而解放了 CPU。

首先,核心在 RAM 中為收發資料建立一個環形的緩沖隊列,通常叫 DMA 環形緩沖區,又叫 BD(Buffer descriptor)表。

核心将這個緩沖區通過 DMA 映射,把這個隊列交給網卡;

網卡收到資料,先把資料臨時存放到 Rx FIFO 中,繞後通過 DMA 的方式把Rx FIFO 的資料包放到RAM 的環形緩沖區中。

然後,網卡驅動向系統産生一個硬中斷,核心收到這個硬中斷後,啟動軟中斷,在軟中斷中關閉硬中斷,告訴 CPU 後續再由資料不用産生硬中斷通知CPU,然後核心線程在軟中斷中從喚醒緩沖區取出資料,上傳到協定棧中

網卡接收資料流程
  • 網卡驅動申請 Rx descriptor ring,本質是一緻性 DMA 記憶體,儲存了若幹的 descriptor。将 Rx descriptor ring 的總線位址寫入網卡寄存器 RDBA。
  • 網卡驅動為每個 descriptor 配置設定 skb_buff 資料緩存區,本質上是在記憶體中配置設定的一片緩沖區用來接收資料幀。将資料緩存區的總線位址儲存到 descriptor。
  • 網卡接收到高低電信号。
  • PHY 晶片首先進行數模轉換,即将電信号轉換為比特流。
  • MAC 晶片再将比特流轉換為資料幀(Frame)。
  • 網卡驅動将資料幀寫入 Rx FIFO。
  • 網卡驅動找到 Rx descriptor ring 中下一個将要使用的 descriptor。
  • 網卡驅動使用 DMA 通過 PCI 總線将 Rx FIFO 中的資料包複制到 descriptor 儲存的總線位址指向的資料緩存區中。其實就是複制到 skb_buff 中。
  • 因為是 DMA 寫入,是以核心并沒有監控資料幀的寫入情況。是以在複制完後,需要由網卡驅動啟動硬中斷通知 CPU 資料緩存區中已經有新的資料幀了。每一個硬體中斷會對應一個中斷号,CPU 執行硬下述中斷函數。實際上,硬中斷的中斷處理程式,最終是通過調用網卡驅動程式來完成的。硬中斷觸發的驅動程式首先會暫時禁用網卡硬中斷,意思是告訴網卡再來新的資料就先不要觸發硬中斷了,隻需要把資料幀通過 DMA 拷入主存即可。

總線、裝置和驅動

Linux 裝置模型中有三個重要的概念,那就是總線(bus)、裝置(device)和驅動(driver)。它們對應資料結構分别為struct bus_type、struct device 和 struct device_driver。

總線是處理器與一個或多個裝置之間的通道,在裝置模型中,所有的裝置都要通過總線相連。而驅動則是使總線上的裝置能夠完成它們應該完成的功能。

總線代表同類裝置需要共同遵守的時序,不同總線硬體的通信時序也是不同的,如I2c總線、USB總線、PCI總線...

系統中總線把多個裝置和驅動進行聯系起來。總線的作用就是完成驅動和裝置之間的關聯。

在總線的資料結構bus_type中,

//總線描述符,在這個變量上連結了pci裝置以及支援pci裝置的驅動程式
structbus_type{
constchar*name;//總線類型名稱
/*與該總線相關的子系統*/
structsubsystemsubsys;
/*總線驅動程式的kset*/
structksetdrivers;
/*挂在該總線的所有裝置的kset*/
structksetdevices;
/*挂接在該總線的裝置連結清單*/
structklistklist_devices;
/*與該總線相關的驅動程式連結清單*/
structklistklist_drivers;           
structblocking_notifier_headbus_notifier;           
structbus_attribute*bus_attrs;/*總線屬性*/
/*裝置屬性,指向為每個加入總線的裝置建立的預設屬性連結清單*/
structdevice_attribute*dev_attrs;
/*驅動程式屬性*/
structdriver_attribute*drv_attrs;           
...
};           

有2個字段 sklist_devices、klist_drivers,它們代表了連接配接這個總線上的兩個連結清單,一個是裝置連結清單,一個時裝置驅動連結清單。是以,通過一個總線描述符,就可以擷取到挂在這條總線上的裝置以及支援該總線的不同裝置的驅動程式。

裝置代表真實存在的實體器件,每個器件有自己不同的通信時序,I2C、USB這些都代表不同的時序,這就與總線挂鈎了。

對于 PCI 總線,其初始化的 bus_type 結構如下:

structbus_typepci_bus_type={ .name="pci", .match=pci_bus_match, .uevent=pci_uevent, .probe=pci_device_probe, .remove=pci_device_remove, .suspend=pci_device_suspend, .suspend_late=pci_device_suspend_late, .resume_early=pci_device_resume_early, .resume=pci_device_resume, .shutdown=pci_device_shutdown, .dev_attrs=pci_dev_attrs, };

通用裝置結構在 struct device 中

structdevice{
structklistklist_children;
structklist_nodeknode_parent;/*nodeinsiblinglist*/
structklist_nodeknode_driver;
structklist_nodeknode_bus;
/*裝置的父裝置,該裝置所屬的裝置,通常一個父裝置是某種總線或主要制器,若為NULL,則該裝置為頂層裝置*/
structdevice*parent;           
structkobjectkobj;
charbus_id[BUS_ID_SIZE];/*positiononparentbus*/
unsignedis_registered:1;
structdevice_attributeuevent_attr;
structdevice_attribute*devt_attr;           
structsemaphoresem;/*semaphoretosynchronizecallsto
*itsdriver.
*/
//表示該裝置是連結到哪個總線上
structbus_type*bus;/*typeofbusdeviceison*/
//目前裝置是由哪個驅動程式所驅動的
structdevice_driver*driver;/*whichdriverhasallocatedthis
device*/
void*driver_data;/*dataprivatetothedriver*/
void*platform_data;/*Platformspecificdata,device
coredoesn'ttouchit*/
structdev_pm_infopower;           
...
};           

該結構時裝置的基本結構,當到具體總線裝置,會有一個包含 device 結構的總線裝置結構。比如挂在PCI 總線上的裝置為pci裝置,其結構為 struct pci_dev。

//每個pci裝置都會配置設定一個pci_dev變量,核心就用這個結構來表示一個pci裝置
structpci_dev{
structlist_headglobal_list;/*nodeinlistofallPCIdevices*/
structlist_headbus_list;/*nodeinper-buslist*/
structpci_bus*bus;/*busthisdeviceison*/
structpci_bus*subordinate;/*busthisdevicebridgesto*/           
void*sysdata;/*hookforsys-specificextension*/
structproc_dir_entry*procent;/*deviceentryin/proc/bus/pci*/           
unsignedintdevfn;/*encodeddevice&functionindex*/
unsignedshortvendor;
unsignedshortdevice;
unsignedshortsubsystem_vendor;
unsignedshortsubsystem_device;
unsignedintclass;/*3bytes:(base,sub,prog-if)*/
u8hdr_type;/*PCIheadertype(`multi'flagmaskedout)*/
u8rom_base_reg;/*whichconfigregistercontrolstheROM*/
u8pin;/*whichinterruptpinthisdeviceuses*/           
structpci_driver*driver;/*whichdriverhasallocatedthisdevice*/           
structdevicedev;/*Genericdeviceinterface*/
...
};           

每個PCI 裝置都有一組參數唯一辨別,它們被vendor、device 和class 所辨別,即裝置廠商,裝置型号等。每個 PC I裝置都會配置設定一個 pci_dev 變量,核心就用這個資料結構來表示一個PCI 裝置。

每個 pci 裝置都有自己的配置空間,裡面存儲了一些基本資訊,比如生産商、 IRQ 中斷号、mem 空間和 IO 空間的起始位址和大小。pci_dev 結構中的資訊都是從 pci 裝置配置空間中讀取獲得的。

驅動代表操作裝置的方式和流程,以應用來說,在程式 open 裝置時,接着read 這個這個裝置,驅動就是實作應用通路的具體過程。驅動就是一個通信官和翻譯官,一是通過對soc的控制寄存器程式設計,按總線要求輸出相應時序的指令,與裝置互動,一是對得到資料進行處理,給上層提供特定格式資料。

  • 不同總線的裝置驅動過程不同
  • 同一總線的不同類型裝置驅動不同,光感和加速度
  • 同一總線的同類裝置驅動不同,多點觸控和單點觸控

驅動的通用結構為structdevice_driver

structdevice_driver{

constchar*name;
//指向這個驅動時連結到哪個總線上
structbus_type*bus;           
structcompletionunloaded;
structkobjectkobj;
/*表示目前這個驅動程式可以驅動哪些裝置,一個驅動程式可以支援一個或多個裝置,而一個裝置隻會
綁定一個驅動程式
該注釋不一定正确
*/
structklistklist_devices;
structklist_nodeknode_bus;           
structmodule*owner;           
int(*probe)(structdevice*dev);
...
};           

對于 PCI 總線上的驅動,所有的PCI 驅動程式都必須定義一個pci_driver 結構變量,該結構中包含了基本的裝置驅動結構 device_driver。

/*所有pci驅動程式結構變量,該結構中描述了這個pci驅動程式所提供的不同功能的函數,
同時在這個結構中也包含了device_driver結構,這個結構定義了pci子系統與pci裝置之間的接口*/
structpci_driver{
structlist_headnode;
char*name;
//該字段列出了這個裝置驅動程式所能夠處理的所有pci裝置的id值
conststructpci_device_id*id_table;/*mustbenon-NULLforprobetobecalled*/
int(*probe)(structpci_dev*dev,conststructpci_device_id*id);/*Newdeviceinserted*/
void(*remove)(structpci_dev*dev);/*Deviceremoved(NULLifnotahot-plugcapabledriver)*/
int(*suspend)(structpci_dev*dev,pm_message_tstate);/*Devicesuspended*/
int(*suspend_late)(structpci_dev*dev,pm_message_tstate);
int(*resume_early)(structpci_dev*dev);
int(*resume)(structpci_dev*dev);/*Devicewokenup*/
int(*enable_wake)(structpci_dev*dev,pci_power_tstate,intenable);/*Enablewakeevent*/
void(*shutdown)(structpci_dev*dev);           
structpci_error_handlers*err_handler;
structdevice_driverdriver;
structpci_dynidsdynids;           
intmultithread_probe;
};           

在該結構中包含了這個PCI驅動程式所提供的不同功能的函數。這個結構定義了PCI子系統與PCI 裝置之間的接口。在注冊 PCI 驅動程式時,這個結構将被初始化,同時這個pci_dirver變量會連接配接到pci_bus_type中的驅動連結清單上去。

在該結構中有個 pci_device_id 結構,它列出了這個裝置驅動程式所能夠處理的所有 PCI 裝置的 ID 值,該結構中包含了 vendor、device、class 等。當後續進行和 PCI 裝置比對時會用到這些值。

PCI 總線、裝置和驅動間的綁定過程

在系統啟動時,PCI 總線會去掃描連接配接到這個總線上的裝置,同時會為每個裝置建立一個pci_dev 結構,在這個結構中有一個device成員,并将這些pci_dev 結構連結到 PCI 總線上的裝置連結清單klist_devices中。

網卡接收資料流程

當往 PCI 總線上注冊裝置驅動時,驅動被加載過程中,系統會初始化pci_driver 結構,最後會調用 driver_register(&drv->driver) 函數把該 PCI 驅動挂載到PCI 總線描述符的驅動連結清單上。同時在注冊的過程中,會根據pci_driver 中的 id_table 中的值去檢視該驅動支援哪些裝置,并将總線上的裝置挂載到該驅動的裝置連結清單上。

網卡接收資料流程

網絡子產品初始化

系統中通過 subsys_initcall 調用來初始化各個子系統的。是以網絡的初始化為 subsys_initcall(net_dev_init);

//網絡裝置初始化,主要包括初始化softnet_data,注冊收發包軟中斷等
staticint__initnet_dev_init(void)
{
inti,rc=-ENOMEM;           
...           
//初始化協定類型連結清單
INIT_LIST_HEAD(&ptype_all);
//初始化協定類型hash表
for(i=0;i<16;i++)
INIT_LIST_HEAD(&ptype_base[i]);           
for(i=0;i<ARRAY_SIZE(dev_name_head);i++)
INIT_HLIST_HEAD(&dev_name_head[i]);           
for(i=0;i<ARRAY_SIZE(dev_index_head);i++)
INIT_HLIST_HEAD(&dev_index_head[i]);           
/*
*Initialisethepacketreceivequeues.
*/
//初始化資料包接收隊列
for_each_possible_cpu(i){
structsoftnet_data*queue;           
//每個CPU建立一個structsoftnet_data變量
queue=&per_cpu(softnet_data,i);
skb_queue_head_init(&queue->input_pkt_queue);
queue->completion_queue=NULL;           
//初始化網絡裝置輪詢隊列
INIT_LIST_HEAD(&queue->poll_list);
set_bit(__LINK_STATE_START,&queue->backlog_dev.state);           
//支援非napi虛拟裝置的回調和配額設定
queue->backlog_dev.weight=weight_p;
queue->backlog_dev.poll=process_backlog;           
atomic_set(&queue->backlog_dev.refcnt,1);
}           
netdev_dma_register();           
dev_boot_phase=0;           
//注冊發送軟中斷
open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
//注冊接收軟中斷
open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);           
...           
}           
subsys_initcall(net_dev_init);           

在這個函數裡,會為每個 CPU 都申請一個 softnet_data 資料結構,在這個資料結構裡的poll_list是等待驅動程式将其poll函數注冊進來。另外 open_softirq 注冊了每一種軟中斷都注冊一個處理函數。

voidopen_softirq (intnr,
void(*action)(structsoftirq_action*),void*data)
{
softirq_vec[nr].data=data;
softirq_vec[nr].action=action;
}           

open_softirq 注冊的方式是記錄在 softirq_vec 變量裡的,後面 ksoftirqd 線程收到軟中斷的時候,也會使用這個變量來找到每一種軟中斷對應的處理函數。

網卡驅動初始化

每一個驅動程式(不僅僅隻是網卡驅動)會使用 module_init 向核心注冊一個初始化函數,當驅動被加載時,核心會調用這個函數。

下面以 e1000 為例。

staticstructpci_drivere1000_driver={
.name=e1000_driver_name,
.id_table=e1000_pci_tbl,
.probe=e1000_probe,
.remove=__devexit_p(e1000_remove),
#ifdefCONFIG_PM
/*PowerManagmentHooks*/
.suspend=e1000_suspend,
.resume=e1000_resume,
#endif
.shutdown=e1000_shutdown,
.err_handler=&e1000_err_handler
};           
staticint__inite1000_init_module(void)
{
...
ret=pci_register_driver(&e1000_driver);
...
}           
module_init(e1000_init_module);           

驅動的 pci_register_driver 調用完成後,Linux 核心就知道了該驅動的相關資訊,比如 e1000 網卡驅動的 e1000_driver_name 和 e1000_probe 函數位址等等。當網卡裝置被識别以後,核心會調用其驅動的probe方法(e1000_driver 的 probe 方法是 e1000_probe)進行加載網卡驅動。驅動 probe 方法執行的目的就是讓裝置 ready。prob 調用流程如下:

pci_register_driver//注冊PCI網卡裝置
->__pci_register_driver
->driver_register
->bus_add_driver//獲得目前裝置所在的總線
->driver_attach//周遊PCI總線上的每個裝置,若某裝置能夠被目前驅動所支援則在裝置與驅動之間建立聯系
->__driver_attach
->driver_probe_device//将裝置與驅動聯系起來
->really_probe//該函數最終會執行這個PCI驅動程式所對應的probe函數,為該裝置進行初始化
->dev->bus->probe()=>pci_device_probe//dev->bus為pci_bus_type,probe為pci_device_probe
->__pci_device_probe//在檢查目前PCI驅動與PCI裝置是否相比對,若比對就調用最後的probe函數
->pci_call_probe
->drv->probe//e1000來說,即為e1000_probe           

通過 pci_register_driver 名字可以知道,e1000 網卡驅動是被注冊到PCI總線上。在 bus_add_driver 中把驅動加載到總線上後,就會調用 e1000de probe 函數,該函數為 e1000 驅動的入口函數(相當于 c 程式的 main 方法),網卡驅動在其 probe() 函數裡面會生成申請一個資料塊,該資料塊包含net_device結構體 和 struct e1000_adapter,同時對該2個資料庫進行初始化。

staticint__devinit
e1000_probe(structpci_dev*pdev,
conststructpci_device_id*ent)
{
...
//該方法中主要用來配置設定I/O和memory和中斷向量号等PCI裝置必要的配置資訊
pci_enable_device(pdev)           
pci_request_regions(pdev,e1000_driver_name)           
pci_set_master(pdev);           
err=-ENOMEM;
//用該函數生成結構體net_device,該結構體就表示網卡裝置
netdev=alloc_etherdev(sizeof(structe1000_adapter));           
...           
/*該函數傳回6個PCI IO區域中的第bar個的基位址值(存儲器域的實體位址)。bar代表基位址寄存器(base address register),取值為0到5.
當驅動程式獲得 BAR 空間在存儲器域的實體位址後,再使用 ioremap 函數将這個實體位址轉換為虛拟位址。
*/
mmio_start=pci_resource_start(pdev,BAR_0);
mmio_len=pci_resource_len(pdev,BAR_0);           
err=-EIO;
adapter->hw.hw_addr=ioremap(mmio_start,mmio_len);           
//驅動初始化
netdev->open=&e1000_open;
netdev->stop=&e1000_close;
netdev->hard_start_xmit=&e1000_xmit_frame;
...           
//注冊poll函數為e1000_clean(),當網卡觸發中斷時,不停的輪訓觸發e1000_clean回調
netdev->poll=&e1000_clean;           
/*setuptheprivatestructure*/
//初始化e1000_adapter結構
e1000_sw_init(adapter)           
...
returnerr;
}           

對 e1000_adapter 的初始化包括對其中的 e1000_hw 結構的初始化。

其中從 pci_dev結構的資源字段中擷取BAR_0 寄存器中儲存的網卡 mem的實體基位址,然後通過 ioremap 完成實體位址到虛拟位址的映射,adapter->hw.hw_addr 儲存映射後的虛拟位址,映射完成後,cpu 可用通過虛拟位址對 網卡mem 空間的通路。

BAR(基址位址寄存器)是儲存于網卡配置空間的資訊,網卡的配置空間 CPU 是不能通路的,隻有 BIOS 能通路,在作業系統啟動過程中,BIOS 會通路總線上所有的裝置,然後給每個裝置在配置設定 pci 總線位址(在 x86 中總線位址就是實體位址,是以就可以直接了解為實體位址。而在其他系統中總線位址不一定就是實體位址,但二者之間有個轉換),而實體位址儲存在 BAR 寄存器中(配置空間的 bar 會有多個,每個 bar 記錄不同的位址可範圍)。在BAR_0 寄存器中辨別網卡 mem 空間的實體位址範圍。

是以在 pci_resource_start(pdev, BAR_0) 中擷取網卡 memory 的實體位址及範圍,這個時候 CPU 還不能通路網卡的 mem 空間, 因為 CPU 隻通過虛拟位址來進行通路。

通過 ioremap 映射來完成網卡 mem 空間的實體位址到虛拟位址進行映射。這時CPU 就可以通過獲得的虛拟位址來通路裝置的 mem 空間。

網卡接收資料流程

網卡的 mem 空間不是我們常說的主存或記憶體,它是存在于裝置網卡内的一段空間。網卡的 mem 空間就是一些寄存器,是以後續進行 DMA 時, CPU 從 adapter->hw.hw_addr 擷取到虛拟位址,會把 DMA 相關的資訊寫入到網卡 mem 空間中。

在對 e1000_hw 的初始化過程中使用了 ioremap() 實作了網卡硬體位址與記憶體虛拟位址之間的映射, 映射完之後,CPU 就可以通過虛拟位址通路網卡硬體位址資訊。

net_device 結構體,用來描述網卡,以及提供操作網卡的接口。同時把該網卡裝置存到發全局網絡裝置連結清單 dev_base 中。

網卡接收資料流程

net_device 資料結構存儲着特定網絡裝置的所有資訊。無論是真是裝置(如 Ethernet)或虛拟裝置(如 Bonding 或 VLAN)。

net_device結構的字段分為:配置、統計資料、裝置狀态、清單管理、流量管理、功能專用、通用、函數指針。

structnet_device
{           
charname[IFNAMSIZ];//網卡裝置名稱,如:eth0
/*devicenamehashchain*/
structhlist_nodename_hlist;
//該裝置的記憶體結束位址
unsignedlongmem_end;/*sharedmemend*/
//該裝置的記憶體起始位址
unsignedlongmem_start;/*sharedmemstart*/
//該裝置的記憶體I/O基位址
unsignedlongbase_addr;/*deviceI/Oaddress*/
//該裝置的中斷号
unsignedintirq;/*deviceIRQnumber*/
//該字段僅針對多端口裝置,用于指定使用的端口類型
unsignedcharif_port;/*SelectableAUI,TP,..*/
//該裝置所使用的DMA通道,開啟DMA:enable_dma 關閉DMA:disable_dma
unsignedchardma;/*DMAchannel*/
//用于存儲其他一些裝置功能。features可報告适配卡的功能,以便與CPU通信
unsignedlongfeatures;
//獨一無二的ID,當裝置以dev_new_index注冊時配置設定給每個裝置
intifindex;           
//擷取流量的統計資訊,運作ifconfig便會調用該成員函數,并傳回一個net_device_stats結構體擷取資訊
structnet_device_stats*(*get_stats)(structnet_device*dev);
//最大傳輸單元,也叫最大資料包
unsignedmtu;/*interfaceMTUvalue*/
//接口的硬體類型
unsignedshorttype;/*interfacehardwaretype*/
//硬體幀頭長度,在以太網裝置的初始化函數中一般被賦為ETH_HLEN,即14
unsignedshorthard_header_len;/*hardwarehdrlength*/
structnet_device*master;
//資料包發送函數, 以使得驅動程式能擷取從上層傳遞下來的資料包。
int(*hard_start_xmit)(structsk_buff*skb,
structnet_device*dev);
/*Thesemaybeneededforfuturenetwork-power-downcode.*/
unsignedlongtrans_start;/*Time(injiffies)oflastTx*/           
intwatchdog_timeo;/*usedbydev_watchdog()*/
structtimer_listwatchdog_timer;           
//打開接口.任何時候ifconfig激活它,接口被打開
int(*open)(structnet_device*dev);
//停止接口.接口停止當它被關閉.這個函數應當恢複在打開時進行的操作
int(*stop)(structnet_device*dev);           
/*用之前取到的源和目的硬體位址來建立硬體頭的函數(在hard_start_xmit前調用).它的工作是将作為參數傳給它的資訊組織成一個合适的特定于裝置的硬體頭.eth_header是以太網類型接口的預設函數,ether_setup針對性地對這個成員指派.*/
int(*hard_header)(structsk_buff*skb,
structnet_device*dev,
unsignedshorttype,
void*daddr,
void*saddr,
unsignedlen);
/*用來在ARP解析完成後但是在封包發送前重建硬體頭的函數.以太網裝置使用的預設的函數使用ARP支援代碼來填充封包缺失的資訊.*/
int(*rebuild_header)(structsk_buff*skb);           
//發包逾時處理函數,需采取重新啟動資料包發送過程或重新啟動硬體等政策來恢複網絡裝置到正常狀态
void(*tx_timeout)(structnet_device*dev);           
...
};           

啟動 e1000 網卡

當使用者啟動一個網卡時(比如執行ifconfig eth0 up),最終調用網卡驅動的open 函數,e1000 對應的方法為 e1000_open。

使用者調用 ifconfig 等程式,然後通過 ioctl 系統調用進入核心,

sock_ioctl//socket的ioctl()系統調用
->dev_ioctl
->dev_ifsioc
->dev_change_flags
->dev_open
->dev->open//e1000_open
/**啟動網卡,通過使用者ifconfigup指令*/
staticinte1000_open(structnet_device*netdev)
{
/*配置設定接收和發送環形緩沖區ringbuff一緻性dma記憶體,并初始化*/
e1000_setup_all_tx_resources(adapter);//配置設定tx資源(描述符,一緻性DMA)           
e1000_setup_all_rx_resources(adapter);           
//注冊中斷處理函數e1000_intr()
err=e1000_request_irq(adapter);           
e1000_power_up_phy(adapter);           
/*把環形緩沖區DMA資訊(比如環形緩沖區的收尾指針,環形緩沖區首位址等)寫到硬體寄存器
中,以便後續網卡收到資料包後進行DMA到記憶體中;
為每個環形緩沖區配置設定skb記憶體;
同時開啟硬中斷*/
e1000_up(adapter)           
...
returnE1000_SUCCESS;           
}           

e1000_open 的執行過程如下:

e1000_open//構造環形緩沖區
|->e1000_setup_all_tx_resources////給txbd配置設定一緻性dma記憶體
|->e1000_setup_tx_resources
|->pci_alloc_consistent//申請一塊DMA可使用的一緻性dma記憶體
|->e1000_setup_all_rx_resources//給rxbd配置設定一緻性dma記憶體
|->e1000_setup_rx_resources
|->pci_alloc_consistent
|->e1000_request_irq//注冊中斷處理函數e1000_intr()
|->e1000_up
|->e1000_configure_tx//配置寄存器,把發送環形緩沖區dma資訊寫入網卡dma寄存器.
|->e1000_configure_rx//把接收環形緩沖區dma資訊寫入網卡dma寄存器.
|->adapter->clean_rx=e1000_clean_rx_irq//設定軟中斷處理接收資料包接口
//為rx_ring中的每一個元素配置設定一個sk_buff,并為每個skb->data建立流式映射
|->adapter->alloc_rx_buf=>e1000_alloc_rx_buffers
在e1000_open 中完成了如下功能:           
  • 分别配置設定一塊一緻性DMA記憶體給發送和接收環形緩沖區;
  • 注冊硬中斷回調函數函數;
  • 設定硬體寄存器,把DMA位址等資訊寫入到網卡寄存器中,以便後續網卡收到消息後能夠找到DMA位址
  • 為每個環形緩沖區的元素配置設定一個sk_buff, 同時為每個skb->data 建立流式映射。
網卡接收資料流程

在 e1000_configure_tx 和 e1000_configure_rx 中會把發送和接收的環形緩沖區的 DMA 的實體基位址、描述符環形隊列的長度等資訊寫入到網卡 mem空間對應的寄存器中。

比如 cpu 把發送環形緩沖區描述符的起始位址寫入到網卡 mem空間的寄存器中,當網卡收到資料後網卡可以通過該寄存器找到基位址進行 DMA

網卡接收資料流程

發送和接收喚緩沖區如下圖:

網卡接收資料流程

以上完成了所有的資源配置設定,就等資料的到來。

NAPI

NAPI(New API)是一種硬中斷和軟中斷輪詢相結合的技術。就是在第一個包到來的時候中斷,然後關閉中斷開始輪詢,等某一次輪詢完畢後發現沒有資料了,那麼核心預設此次資料已經傳輸完畢,短時間内不會 再有資料了,那麼停止輪詢,重新開啟中斷。

NAPI技術解決了硬中斷過多導緻資料丢失或cpu負載壓力過大和當沒有資料時輪詢浪費cpu的缺點。

中斷

中斷分為硬中斷(上半部)和軟中斷(下半部)。

  • 硬中斷是主要是儲存資料,然後通知cpu有資料到來。
  • 軟中斷是cpu接收資料進行處理資料。

中斷的注冊

網絡子系統啟動時,注冊軟中斷
net_dev_init
|->open_softirq(NET_TX_SOFTIRQ,net_tx_action,NULL);
|->open_softirq(NET_RX_SOFTIRQ,net_rx_action,NULL);           
sock_ioctl//socket的ioctl()系統調用
->dev_ioctl
->dev_ifsioc
->dev_change_flags
->dev_open
->e1000_open
->e1000_request_irq//注冊硬中斷e1000_intr
->request_irq(adapter->pdev->irq,&e1000_intr,...)
->e1000_up
->e1000_configure_rx//設定軟中斷處理接收資料包接口
->adapter->clean_rx=e1000_clean_rx_irq;           

當網卡收到資料後,會把資料包 DMA 到記憶體中,然後觸發硬中斷通知CPU,硬中斷的處理過程:

do_IRQ
->e1000_intr
//寄存器寫為全1,表示屏蔽所有的中斷,禁止網卡硬中斷
->E1000_WRITE_REG(hw,IMC,~0);
->__netif_rx_schedule
->list_add_tail(...);//把網卡的napi挂到CPU的softnet_data上
->__raise_softirq_irqoff(NET_RX_SOFTIRQ)//開啟軟中斷接收處理資料           

在硬中斷中關閉硬中斷,這樣後續網卡收到資料包後,直接 DMA 到記憶體即可,不用觸發硬中斷同時 CPU, 因為這個時候 CPU 正在接收處理。同時把 dev->poll 挂到 CPU 的 softnet_data 上,然後開啟軟中斷。

軟中斷從環形緩沖區中取出 skb,然後傳遞到協定棧,最後重新開啟硬中斷,這個時候網卡收到資料包後,再次觸發硬中斷同時 CPU 接收處。 軟中斷觸發處理接收資料流程:

do_softirq
->__do_softirq
->net_rx_action
->dev->poll=>e1000_clean
->adapter->clean_rx=>e1000_clean_rx_irq //循環處理skb
->netif_receive_skb
->ip_rcv
->...
->e1000_irq_enable//重新開啟硬中斷           

網卡接收消息處理流程如下:

網卡接收資料流程

系統啟動過程中 BIOS 會周遊所有的 PCI 裝置,然後給裝置在實體記憶體空間中配置設定空間。

每個 PCI 裝置都有一塊配置空間和 mem 空間,配置空間是裝置出廠時寫入的裝置及廠家資訊等,配置空間是不允許 CPU 通路的,而 mem 空間是由網卡的一系列寄存器組成,經過 ioremap 是可以允許 CPU 通路的。

BIOS 給 mem 配置設定的實體空間的基位址被寫入到配置空間的 BAR 寄存器中,BIOS 周遊裝置時會給裝置生成一個 pci_dev 記憶體結構,同時會把 mem 空間實體位址資訊儲存到 pci_dev 結構中的 resource 字段中。

當裝置驅動執行時,會從 resource 中取出 mem 的基位址和長度資訊,調用 ioremap 完成實體位址到虛拟位址的映射,這樣 CPU 就可以通過虛拟位址像通路記憶體一樣來通路裝置的 mem 空間。此時網卡的 mem 空間和 CPU 建立了聯系。

當網卡啟動時,在 xx_open 中會在記憶體中生成一些資源資訊用于接收資料包,也即是在記憶體中建構接收和發送的環形緩沖區,而該環形緩沖區是通過 pci_alloc_consistent 申請的一塊一緻性 DMA 記憶體,該函數傳回2個位址,一個是該記憶體對應的虛拟位址基位址,另一個是該記憶體對應的 DMA 位址。虛拟位址供 CPU 使用,DMA 位址供網卡中的 DMA 控制器使用。而該環形緩沖區的基位址和長度以及收尾指針會寫入到 mem 空間的寄存器中。

這樣當網卡接收光/電信号,将其轉換為資料幀内容,如果幀符合以太網位址等過濾條件,則儲存到RX FIFO緩存對列中。

網卡解析FIFO中資料幀的2/3/4層資訊,進行流過濾、流定向、RSS隊列分流,計算出幀對應的分流隊列号。

網卡從 mem 空間的寄存器中擷取環形緩沖區的資訊,DMA 控制器把接收的資料包直接DMA到記憶體中,該過程都是由網卡硬體完成的。

當網卡把一個資料包 DMA 到記憶體中後會觸發硬中斷通知 CPU 有資料到來後,系統響應硬中斷,在硬中斷中關閉硬中斷,然後觸發軟中斷處理資料包。關閉硬中斷的原因是 CPU 這個時候已經開始處理資料包,後續再有資料包到來時不用同時 CPU,可以直接 DMA 到記憶體中。

在軟中斷中,CPU 從環形緩沖區中讀取一個個 skb,然後把 skb 傳輸到協定棧中進行處理。處理完一定數量後,開啟硬中斷,這樣網卡再收到資料後通過硬中斷通知 CPU 進行處理。

https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzg2NDUyNjYyMg%3D%3D%26mid%3D2247484871%26idx%3D1%26sn%3Df7cbe6b0cd4c045adf8dba47b5defb4f%26chksm%3Dce694db6f91ec4a024bd62b39a43c74ab9bfaffb595e3733a76ad546f82072d63ba63d933657%26token%3D453035962%26lang%3Dzh_CN%23rd)

原文連結:https://zhuanlan.zhihu.com/p/553904728

繼續閱讀