网卡(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*/ //当前设备是由哪个