天天看点

网卡接收数据流程

作者:后端开发进阶

网卡(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

继续阅读