(绕蒙圈了已经)可直接跳到后面看结论.目前已经不影响项目进展,中间部分当作素材,留着以后看看能不能整理出来吧。
这里主要是想搞清楚具体的,从网卡拿到数据包的流程是什么样的。正常来说,数据包进入网卡队列,由网卡驱动将数据包拷贝到内核缓冲区,然后进行封装,封装成sk_buffer然后就上交内核协议栈了。那么在我们的程序中,我们想要直接拿到网卡队列中的原始数据包。
0.DPDK为我们提供的接口
想要达到这样的目的很简单,因为dpdk已经为我们提供了这样的接口:
1 | static inline uint16_t |
其中,第一个参数代表我们想要从哪个网卡端口获取数据包,第二个参数代表我们想要从哪个队列获取数据包,第三个参数是存放数据包数组指针,这个结构体用于存放原始数据包,第四个参数用于限制依次读取数据包的大小。
1.接口中重要的两步
在上一部分提到的接口函数的定义中找到两句比较重要的。
1 | struct rte_eth_dev *dev = &rte_eth_devices[port_id]; |
通过port_id
拿到我们具体想要在哪个网卡端口获取数据包。代表网卡端口设备的结构体定义
1 | struct rte_eth_dev { |
这个结构体与每一个以太网设备关联。可以这样理解,这就是网卡的用户态驱动程序。可以看到其中就定义了我们接收数据包所需要的rx_pkt_burst
。也就是说,根据我们实际使用的网卡型号,我们在初始化网络设备的时候,会对这个数据结构进行相应的赋值。以实现对应网卡的对应功能。这个在运行dpdk程序之前需要我们去绑定用户态驱动。这个不用多说,绑定之后由dpdk去为我们检测。比如在程序中我们调用
1 | int rte_eth_dev_count_avail() //就可以查看当前绑定了用户态驱动的网卡设备数量。 |
等等等等。这些信息都源自下面这个结构体,通过下面的结构体存放这绑定了用户态驱动的网卡端口任何信息,
1 | struct rte_eth_dev_info { |
应用程序中往往执行下面的两行代码就可以拿到,
1 | rte_eth_dev_info portInfo; |
id一般从0开始,如果系统中有两个网卡绑定了用户态驱动,那么通过0,1就可以获取这两张网卡设备的信息。
查了一下自己用的igb_uio
驱动的源码,比较重要的映射功能实现主要是下面的probe函数
1 | static int |
1 | err = igbuio_setup_bars(dev, &udev->info); |
函数的作用是读取当前设备的所有PCI BAR
的信息,并存储到uio_info结构体中。PCI BAR
是一种存储基地址的寄存器,它可以指示设备内部的内存或I/O
端口的位置和大小。这些信息在后续注册UIO
设备时需要使用,以便用户程序可以通过mmap
函数映射/dev/uioX
文件到用户空间的虚拟地址,并根据偏移量访问不同的PCI BAR
区域。
看到下面这两个指针的定义就觉得不简单
1 | dma_addr_t map_dma_addr; |
dma_alloc_coherent
是一个用于分配一致性DMA
内存的函数。一致性DMA
内存是指可以被设备和CPU
同时访问而不需要考虑缓存效应的内存。
dma_alloc_coherent
函数接受四个参数:第一个参数dev
是一个struct device
指针,用于指定要执行DMA
的设备;第二个参数size
是要分配的内存大小;第三个参数是一个dma_addr_t
指针,用于返回分配的内存的DMA
地址;flag
是一个gfp_t
类型,用于指定分配内存时的标志位,如GFP_KERNEL
等。
dma_alloc_coherent
函数返回两个值:用于CPU访问的虚拟地址cpu_addr
和用于设备访问的DMA
地址dma_addr_t
。这两个地址可能不相同,因为可能存在物理地址和总线地址之间的转换。因此,在CPU端要使用cpu_addr
来操作分配的内存,在设备端要使用dma_addr_t
来作为DMA
源或目标地址
经历这步之后,打印信息
1 | dev_info(&dev->dev, "mapping 1K dma=%#llx host=%p\n", |
合理推测(不一定对哈),到时候数据包从网卡队列里头拿出来应该就是拿到这里面。
可以将PCI设备的内存空间映射到用户空间,并提供中断处理功能。而PMD驱动程序由具体的网卡驱动程序支持。
那直接总结好了:
首先运行dpdk程序会要求我们将网卡绑定UIO驱动。我绑定的是igb_uio
UIO驱动是一种用户空间驱动,它可以让用户空间的程序直接访问和控制某些类型的设备,而不需要编写复杂的内核模块。UIO驱动的优点是简化了驱动程序的开发和维护,降低了内核崩溃的风险,提高了驱动程序的更新效率。UIO驱动适用于那些具有可映射的内存空间,可以通过内存写入来控制,通常会产生中断,并且不属于标准内核子系统范畴的设备。
UIO驱动的工作原理是通过一个字符设备文件和一些sysfs属性文件来访问和管理设备。字符设备文件通常命名为/dev/uio0, /dev/uio1等等。用户空间的程序可以通过mmap()函数将设备文件映射到自己的地址空间,并通过读写该地址空间来访问和控制设备的寄存器或RAM位置。用户空间的程序还可以通过read()或select()函数来等待和处理设备产生的中断。sysfs属性文件则提供了一些关于设备名称、版本、映射大小、中断号等信息。
(Poll Mode Drivers)网卡驱动。该驱动由用户态的 API 以及 PMD Driver 构成,内核态的 UIO Driver 屏蔽了网卡发出的中断信号,然后由用户态的 PMD Driver 采用主动轮询的方式。除了链路状态通知仍必须采用中断方式以外,均使用无中断方式直接操作网卡设备的接收和发送队列。
PMD Driver 从网卡上接收到数据包后,会直接通过 DMA 方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包,这个过程非常简洁。
PMD 极大提升了网卡 I/O 性能。此外,PMD 还同时支持物理和虚拟两种网络接口,支持 Intel、Cisco、Broadcom、Mellanox、Chelsio 等整个行业生态系统的网卡设备,以及支持基于 KVM、VMware、 Xen 等虚拟化网络接口。PMD 实现了 Intel 1GbE、10GbE 和 40GbE 网卡下基于轮询收发包。
UIO+PMD,前者零拷贝,后者主动轮询避免了硬中断,DPDK 从而可以在用户态进行收发包的处理。带来了零拷贝(Zero Copy)、无系统调用(System call)的优化。同时,还避免了软中断的异步处理,也减少了上下文切换带来的 Cache Miss。
igb_uio是一种PCI驱动,它可以将网卡设备绑定到用户态,并绕开内核的网络栈。它的主要工作流程如下:
- igb_uio模块初始化时,向内核注册一个pci_driver结构体,但不指定任何设备id。
- 用户通过dpdk-devbind.py脚本将某个网卡设备绑定到igb_uio驱动,触发probe回调函数。
- probe回调函数中,首先使能当前设备,并设置DMA模式。
- 然后调用igbuio_setup_bars函数,读取当前设备的所有PCI BAR的信息,并存储到uio_info结构体中。PCI BAR是一种存储基地址的寄存器,它可以指示设备内部的内存或I/O端口的位置和大小。
- 接着初始化UIO设备的文件操作函数,如open、release、irqcontrol等。
- 最后调用uio_register_device函数,将当前设备注册为UIO设备,并在sysfs文件系统下创建/dev/uioX文件接口。
- 用户程序可以通过mmap函数,映射/dev/uioX文件到用户空间的虚拟地址,并根据偏移量访问不同的PCI BAR区域。这样就可以直接与网卡设备进行通信了。
DPDK是一种数据平面开发工具包,它可以实现绕过内核的功能,提高网络I/O性能。DPDK的主要方法是:
- 使用UIO或VFIO等机制,将网卡设备从内核驱动转移到用户态驱动,这样可以避免中断和系统调用的开销,直接访问网卡寄存器和描述符123。
- 使用大页内存和物理连续的内存池,分配和管理数据包缓冲区,这样可以减少TLB的开销,提高缓存命中率,降低延迟45。
- 使用轮询模式的驱动,不依赖于中断,而是主动检查网卡的状态,并处理数据包3。
通过这些方法,DPDK可以实现不在内核的高性能网络I/O。
结论!!!!
网卡设备绑定igb_uio之后会将自己的硬件内部的内存空间 保存在sysfs的目录下,用户可以将这部分空间映射到用户空间。
1 | static inline uint16_t |
所以说这个接口底层逻辑就是通过PMD
直接从网卡的硬件获取数据包保存在rte_mbuf
中
至于PMD的好处!
PMD可以直接把网卡上的数据包存放到用户空间,是因为它使用了一些特殊的机制,如:
- UIO或VFIO等机制,可以将网卡设备从内核驱动转移到用户态驱动,这样可以避免中断和系统调用的开销,直接访问网卡寄存器和描述符
- 大页内存和物理连续的内存池,可以分配和管理数据包缓冲区,这样可以减少TLB的开销,提高缓存命中率,降低延迟。
通过这些机制,PMD可以实现不经过内核的高效数据传输。