目录
linux收包的方式
Linux 内核在收包时有两种方式可供选择,一种是中断方式,另外一种是轮询方式。
从哲学的角度来说,中断是外界强加给你的信号,你必须被动应对,而轮询则是你主动地处理事情。前者最大的影响就是打断你当前工作的连续性,而后者则不会,事务的安排自在掌握。
中断对性能的影响有多大?
在 x86 体系结构中,一次中断处理需要:
将 CPU 的状态寄存器保存到堆栈;
运行中断服务程序;
再将保存的状态寄存器信息从堆栈中恢复。
整个过程需要至少 300 个处理器时钟周期。
所以:
网络中大量数据包到来时,会频繁产生中断请求,频繁的中断会造成上下文的切换产生时延,产生较高的性能开销。
轮询对性能的提升有多大?
网卡收到报文后,可以借助 DDIO(Direct Data I/O)技术直接将报文保存到 CPU 的 Cache 中,或者保存到内存中(没有 DDIO 技术的情况下),并设置报文到达的标志位。应用程序则可以周期性地轮询报文到达的标志位,检测是否有新报文需要处理。整个过程中完全没有中断处理过程,因此应用程序的网络报文处理能力得以极大提升。
- 当前 Linux 内核为什么不使用轮询模式
轮询模式,会一直占用CPU去查询是否有包达到,即使在没有包到达的情况下。当前的Linux的CPU,不仅仅是收发包,还要为应用程序预留计算资源。所以轮询模式,适用于仅仅进行包转发,没有到应用层的的情况下。
当前内核协议栈可以使用 NAPI (中断+轮询)的方式,但依旧没有根本上解决问题。
这种PMD驱动并不适用于一般的应用场景,因为需要有专门的CPU给PMD一直做轮询操作,对应的CPU就会一直占用100%,可能严重影响了其他任务的运行。
当网络处于空闲状态的时候,CPU占用100%的问题会带来额外的不必要的功耗,因此dpdk还推出了一个interrupt dpdk模式,即当网卡中没有数据包处理的时候进入类似睡眠模式的状态,然后改为传统的中断方式通知,这个时候被100%占用的核心利用率就会降低,可以和其他的进程共享,但是DPDK仍然拥有高优先级,而当有数据包进来的时候还是可以优先处理。
PMD 介绍
PMD是Poll Mode Driver的缩写,即基于用户态的轮询机制的驱动。
参见:dpdk pmd 介绍
- 我的理解
- PMD 包含 PMD 应用程序(DPDK程序) + PMD KMOD(pmd kmod: 比如:igb_uio/uio_pci_generic/vfio_pci)
- PMD应用程序「DPDK程序」在用户态调用API 接口对网卡进行配置,获取统计,收发包。
用户态部分实现真正的业务处理,内核态部分主要是接管硬件资源提供给用户态部分使用。
如上:我将整个框架分了四层,用户层、接口层、内核层、硬件层。网卡开始是与e1000等网卡驱动绑定,当用户层程序执行解绑以及绑定命令,通过sysfs将消息发送至内核层后,网卡与igb_uio驱动进行了绑定。此时内核层uio接管了网卡,并为用户层pmd提供了服务接口。到此我们知道pmd的使用是需要内核层支持的。
- DPDK应用如何基于PCI获取对应网卡驱动的配置、查询、收发包的API的?
1> DPDK程序 通过-w/-a 参数指定 pci
2> 基于pci 查询 该PCI上设备网卡对应的 Vendor-ID, Device-ID,进而可以知道对应的驱动。
3> 注册对应驱动的 DPDK API接口。
比如:识别为 ixgbe 驱动,rte_eth_dev_filter_ctrl 赋值为 ixgbe_dev_filter_ctrl。
linux 下基于 pci查询 vendor-id, device-id:
8086 为 vendor-id, 代表 intel.
10fb 为 device-id, 代表 82599ES.
参见:intel 8259x 系列网卡的 vendor-id,device-id 对照表
查看某个DPDK版本支持的驱动、网卡:
./dpdk-pmdinfo.py -t ./dpvs
由于 dpvs 是通过制定版本的 dpdk 编译而成。
- 驱动对应的DPDK程序调用的用户态的API构成的DPDK lib库
如下所示:用户态的一些库,提供了 API,共DPDK应用程序进行调用。
DPDK程序需要包含这些库,才可以调用对应的API。
收包对比
PMD驱动包含了各种API,在用户态配置网卡和网卡的收发队列。
PWD驱动最大的优势就是无需任何中断操作就可以直接访问网卡队列中的RX/TX描述符(除了网卡的链接状态变化),借助这个优势,就可以绕过内核和同样运行在用户态的应用程序快速地进行数据传输。
这种绕过系统内核的做法最直接的代价就是驱动程序必须一直通过轮询poll的操作来保证能够及时接收到网卡的信息和数据,导致对应的cpu核心会一直处于100%的占用状态。同时这个时候网卡已经不再归于系统内核管理,常规的ip和ifconfig等命令已经没办法查看网卡的详细信息
内核收包的弊端
- 中断处理:
当网络中大量数据包到来时,会频繁产生中断请求,频繁的中断会产生较高的性能开销、并造成上下文的切换产生时延。
- 内存拷贝
网络数据包到来时,网卡通过 DMA 等拷贝到内核缓冲区,内核协议栈再从内核空间拷贝到用户态空间,在 Linux 内核协议栈中,这个耗时操作甚至占到了数据包整个处理流程的 57.1%。
- 局部性失效
目前主流处理器都是多个CPU核心的,这意味着一个数据包的处理可能跨多个 CPU 核心。比如:一个数据包可能中断在 cpu0,内核态处理在 cpu1,用户态处理在 cpu2,这样跨多个核心,容易造成 CPU 缓存失效,造成局部性失效。
DPDK 收包的优点
- 整体流程
PMD Driver 从网卡上接收到数据包后,会直接通过 DMA 方式传输到预分配的内存中,同时更新无锁环形队列中的数据包指针,不断轮询的应用程序很快就能感知收到数据包,并在预分配的内存地址上直接处理数据包(减少内存拷贝),这个过程非常简洁。
- 优点
- 内核bypass
通过UIO(Userspace I/O)旁路数据包,实现用户态数据包收发,减少内存拷贝;另外轮询方式收包代替中断方式,减少上下文切换。
- CPU 的亲和性
设置 CPU 的亲和性,将线程和 CPU 核进行一比一绑定,减少彼此之间调度切换,减少cache miss.
- 使用大页内存代替普通的内存
减少 cache-miss。
- 采用无锁技术
解决资源竞争问题。
uio/igb_uio/uio_pci_generic/vfio-pci
为了支持Userspace IO,DPDK可以选择如下三种类型的驱动:
- uio_pci_generic
- igb_uio
- vfio-pci
参见:Userspace IO
注:
igb_uio 完全可以代替 uio_pci_generic;
Mellanox 网卡比较特殊,不需要igb_uio等内核模块。
igb_uio
PMD是在用户态提供的API来操作,但还是依赖于内核提供的策略。
- uio模块
是内核提供的用户态驱动框架。 - igb_uio/uio_pci_generic
IGB_UIO/uio_pci_generic(igb_uio.ko)是DPDK 用于与 UIO 交互的内核模块,通过 IGB_UIO/uio_pci_generic 来 bind 指定的 PCI 网卡设备给到用户态的 PMD 使用。可以认为 igb_uio/uio_pci_generic 是 uio 模块的一个实例。
IGB_UIO/uio_pci_generic 借助 UIO 技术来截获中断,并重设中断回调行为,从而绕过内核协议栈后续的处理流程。
IGB_UIO/uio_pci_generic 会在内核初始化的过程中将网卡硬件寄存器映射到用户态。
igb_uio/uio_pci_generic 是一种 pci 驱动,将网卡绑定到 igb_uio 隔离了网卡的内核驱动,同时 igb_uio 完成网卡中断内核态初始化并将中断信号映射到用户态。
比如:网卡在内核中使用 ice/ixgbe 内核驱动,如果通过 dpdk-devbind 解绑定内核驱动 ice/ixgbe, 然后使用 igb_uio进行接管网卡。这样dpdk应用程序会使用dpdk的 ice/ixgbe pmd ,底层是igb_uio /uio 来进行收发包。
dpdk的 ice/ixgbe pmd 代码可以在dpdk 源码中查看。
注:一旦dpdk应用使用 ice/ixgbe pmd + igb_uio + uio 进行收发包,那么和内核原生的驱动 ixgbe/ice 就没有关系了。
IGB_UIO 内核模块主要功能:
- 用于注册一个 PCI 设备。
通过 DPDK 提供个 Python 脚本 dpdk-devbind 来完成,当执行 dpdk-devbind 来 bind 网卡时,会通过 sysfs 与内核交互,让内核使用指定的驱动程序(e.g. igb_uio)来绑定网卡。
在 Linux 中,将设备(Device)与驱动(Driver)进行绑定的方法有两种:
- 配置设备
让其选择驱动:向 /sys/bus/pci/devices/{pci id}/driver_override 写入指定驱动的名称。- 配置驱动
让其支持新的 PCI 设备:向 /sys/bus/pci/drivers/igb_uio/new_id 写入要 bind 的网卡设备的 PCI ID(e.g. 8086 10f5,格式为:设备厂商号 设备号)。
按照内核的文档 https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci 中提到,这两个动作都会促使驱动程序 bind 新的网卡设备,而 DPDK 显然是使用了第 2 种方式。
- 让用户态的 PMD 网卡驱动程序(DPDK程序)得以与 UIO 进行交互
- 调用 igbuio_setup_bars,设置 uio_info 的 uio_mem 和 uio_port。
- 设置 uio_info 的其他成员。
- 调用 uio_register_device,注册 UIO 设备。
- 打开 UIO 设备并注册中断。
- 调用 uio_event_notify,将注册的 UIO 设备的 “内存空间” 映射到用户态的应用空间。
其 mmap 的函数为 uio_mmap。至此,UIO 就可以让 PMD 驱动程序在用户态应用层访问设备的大部分资源了。- 应用层 UIO 初始化。同时,DPDK 还需要把 PCI 设备的 BAR 映射到应用层。
在 pci_uio_map_resource 函数中会调用 pci_uio_map_resource_by_index 做资源映射。- 在 PMD 驱动程序中,DPDK 应用程序,会调用 rte_eth_rx_burst 读取数据报文。如果网卡接收 Buffer 的描述符表示已经完成一个报文的接收(e.g. 有 E1000_RXD_STAT_DD 标志),则 rte_mbuf_raw_alloc 一个 mbuf 进行处理。
- 对应 RTC 模型的 DPDK 应用程序来说,就是不断的调用 rte_eth_rx_burst 去询问网卡是否有新的报文。如果有,就取走所有的报文或达到参数 nb_pkts 的上限。然后进行报文处理,处理完毕,再次循环。
uio_pci_generic
uio_pci_generic是内核原生的一种uio驱动,该驱动提供了uio功能,直接使用如下命令加载:
sudo modprobe uio_pci_generic
uio_pci_generic 和 igb_uio对比
- uio_pci_generic 是不支持VF设备创建的, igb_uio 支持
igb_uio可以用于宿主机上来创建VF设备。适用性比内核原生的uio_pci_generic更强一些, - igb_uio.ko是由 dpdk 代码库编译出来的,uio_pci_generic 是原生的,内核自带的。
sudo modprobe uio
sudo insmod kmod/igb_uio.ko
vfio-pci
从DPDK release 1.7开始,DPDK对VFIO进行了支持,因此VFIO Driver成了新的可选项:
sudo modprobe vfio-pci
特别注意:
对于使用VFIO驱动来使用DPDK的场景,必须保证:
1.硬件上支持支持VT-x、VT-d,BIOS中需要打开相关特性
2.对于物理机的内核中需要支持IOMMU特性(在启动参数添加 iommu=pt, intel_iommu=on)
- 绑定vfio-pci 出错
Bifurcated Driver: 分叉驱动
使用分叉驱动程序的PMD与设备内核驱动程序共存。 在这种模式下,NIC由内核控制,而数据路径则由PMD直接在设备上执行。
注:使用分叉驱动程序的PMD不应从其内核驱动程序中解除绑定。
Mellanox Bifurcated DPDK PMD
参见:Mellanox Bifurcated DPDK PMD
对比
如果正在使用的PMD使用UIO或VFIO驱动程序,那么DPDK应用程序要使用的所有端口都必须在应用程序运行之前绑定到uio_pci_generic、igb_uio或vfio-pci模块。 对于此类PMD,Linux 控制下的任何网络端口都将被忽略,并且不能被应用程序使用。
uio + igb_uio/uio_pci_generic的局限
绑定与解绑定
# 查看
Network devices using DPDK-compatible driver
============================================
0000:82:00.0 '82599EB 10-GbE NIC' drv=uio_pci_generic unused=ixgbe
0000:82:00.1 '82599EB 10-GbE NIC' drv=uio_pci_generic unused=ixgbe
Network devices using kernel driver
===================================
0000:04:00.0 'I350 1-GbE NIC' if=em0 drv=igb unused=uio_pci_generic *Active*
0000:04:00.1 'I350 1-GbE NIC' if=eth1 drv=igb unused=uio_pci_generic
0000:04:00.2 'I350 1-GbE NIC' if=eth2 drv=igb unused=uio_pci_generic
0000:04:00.3 'I350 1-GbE NIC' if=eth3 drv=igb unused=uio_pci_generic
Other network devices
=====================
# 绑定:
./usertools/dpdk-devbind.py --bind=uio_pci_generic 04:00.1
宿主机上使用dpdk
在物理机上使用DPDK,需要内核中加载DPDK PMD Driver,那么需要使用如下命令加载DPDK的驱动:
modprobe uio
insmod igb_uio
usertools/dpdk-devbind.py --bind=igb_uio bb:ss.f
注:这里我们也可以使用上面介绍过的其他类型的内核模块:uio_pci_generic 或者 vfio-pci 。
虚拟机中使用DPDK
对于支持SR-IOV的网卡来说,比如Intel的X710/XL710网卡,在虚拟化的环境中使用,网卡可以进行透传,本文以透传的方式来进行实践介绍,对于支持SR-IOV的网卡来说,它分为PF和VF模块,在宿主机中需要加载对应的PF Driver和VF Driver来驱动这两个子模块。
宿主机
在宿主机上可以直接使用Linux kernel官方的intel PF驱动,比如i40e,也可以使用DPDK专用的 PMD PF驱动。
如果使用了DPDK PMD PF 驱动,那么这个宿主机网络的管理权就完全交给DPDK了。
- 方案一:i40e驱动
rmmod i40e (To remove the i40e module)
insmod i40e.ko max_vfs=2,2 (To enable two Virtual Functions per port)
通过重新加载intel提供的i40e驱动,并指定max_vfs参数来创建VF功能,对于该网卡的VF功能内核默认使用的驱动为 i40evf,因此在使用dpdk之前,还需要在Host上将VF与i40evf驱动解绑,重新绑定到vfio-pci驱动上:
modprobe vfio-pci
usertools/dpdk-devbind.py --bind=vfio-pci bb:ss.f
1> 此时该VF已经由vfio驱动接管,如果在宿主机上使用VFIO DPDK,那么此时就已经满足了条件;
2> 如果为了演示虚拟机中使用DPDK,此时不能启动DPDK去使用该VF,而需要在虚拟机中透传该设备来使用。
宿主机中需要使用vfio_pci这个内核模块来对需要分配给客户机的设备进行隐藏, 从而让宿主机和未被分配该设备的客户机都无法使用该设备, 达到隔离和安全使用的目的。而在客户机不需要使用该设备后, 让宿主机使用该设备, 则需要将其恢复到使用原本的驱动。
- 方案二:DPDK PMD PF驱动
需要内核启动参数中使能iommu=pt, intel_iommu=on,然后启动后加载DPDK驱动:
modprobe uio
insmod kmod/igb_uio.ko
usertools/dpdk-devbind.py --bind=igb_uio bb:ss.f
echo 2 > /sys/bus/pci/devices/0000\:bb\:ss.f/max_vfs (To enable two VFs on a specific PCI device)
虚机
对于虚拟机来说,透传过来的VFIO网卡对于虚拟机来说就相当于是一个常规的物理网卡,默认就会使用该物理网卡对应的驱动,比如i40e driver。
那么如果要在虚拟机中使用DPDK,就需要把虚拟网卡重新绑定到igb_uio驱动,这样就可以在虚拟机中使用DPDK了。实际上操作还是与宿主机中一样:
modprobe uio
insmod kmod/igb_uio.ko
usertools/dpdk-devbind.py --bind=igb_uio bb:ss.f
参考
https://www.daimajiaoliu.com/daima/60f53a20b369801
https://zhuanlan.zhihu.com/p/71563242
https://www.codeleading.com/article/69134740056/