☰
告别内核协议栈:基于DPDK用户态协议栈实现的环境搭建和原理
2026/10/29 11:00:55 网站建设 项目流程

DPDK(Data Plane Development Kit,数据平面开发套件)是一套用于高速网络数据包处理的开发框架。 它的核心目标是尽量服务端减少传统网络收发路径中的开销,让应用程序能够以更高的吞吐量、更低的延迟处理数据包。

传统网络接收包需要经过物理网卡将数据写入到主存缓冲区,触发内核网络中断再到socket接收缓冲区,最后实现内核到用户态的切换才能让数据在应用程序之中读取出来,这样必然涉及到中断、调度开销、内存管理开销以及系统调用和上下文切换。DPDK适用在特定的高性能数据平面场景,是另外的设计取舍。网卡直接把数据写入到预先准备好的内存缓冲区域职中,用户态DPDK Poll Mode Driver主动批量轮询RX队列,获取数据缓冲包,用户态协议栈需要负责解析各类协议,转发过滤统计或者修改报文,TX队列也可以直接将处理之后的报文提交给网卡发送。

本文主要介绍如何搭建DPDK环境搭建,做好初步实现DPDK用户态协议栈项目的准备。

一、网卡配置

多队列在DPDK之中可以真正实现并行处理数据,多队列网卡允许将不同的队列绑定到不同的CPU核心,可以指定从哪个队列之中接收数据从哪个队列发送数据,从而让每一个核心处理处理一部分流量避免锁竞争,

bash终端输入ip addr之后显示如下信息:

这里重点可以看到两条核心消息:1: lo:这是本地回环接口(Loopback),不是真正的网卡。它的 IP 是127.0.0.1,系统内部自己和自己通信用的。2: ens33:这是真实网卡,也可以在这条下面看到该虚拟机在私域网下的IPv4地址和IPv6地址。

输入cat /proc/interrupts | grep ens33查看网卡的中断信息,一般输出一行中断信息就是表明当前的ens33网卡不支持多队列,我们需要修改相关环境配置。

将虚拟机关机之后找到其安装路径下的.vmx后缀配置文件,以及类似的ethernet0.virtualDev = "e1000"的配置信息:

将其修改成为以下内容:

之后再重启虚拟机。此时将e1000的网卡型号转变为支持多队列高性能的vmxnet3网卡型号,网卡的名字也由于硬件的变化从ens33转变成为了ens160

使用新的网卡来执行以下命令:

ls /sys/class/net/ens160/queues/

结果如下:

这条命令可以用来查看队列目录,可以看到执行结果里面显示内核已经为唯一的一块网卡创建了四个接受队列 rx 和四个发送队列 tx。主要原因是vmxnet3网卡驱动默认会将队列与vCPU核心对齐,也就是说你给虚拟机分配了几个核心本次修改完vmx配置文件之后就会分配多少个队列这样做的目的就是为了保证每一个队列都有vCPU资源来处理网络数据。

vCPU是VMware软件假装出来的核心,如果物理机核心足够,虚拟机分配的四个vCPU那么有可能是真正在四个物理核心上面并行运行,如果物理核心不太够,那么VMware会让多个vCPU分时复用多个物理核心。一般而言在主机内核数量多于在VMware上分配的虚拟核心数量的时候基本是在物理核心上面并行,在DPDK压测的时候最好不要让vCPU超过物理机的核心数目。

cat /proc/interrupts | grep ens160

系统给当前网卡分配了5个中断号,中断方式会从之前的IO-APIC变成现代网卡专用的PCI-MSI,ens160-rxtx-0~3就是开启的 4 个队列。ens160-event-4这是网卡的控制事件队列,只负责处理网卡状态变化、链路断开/连接等管理事件,不参与业务数据包传输。

关于网卡的命名,如果Linux系统之中启用了可预测网络接口名称,那么网卡的名字则是以en..开头,表示以太网后面的字母表示网卡的物理位置。如果以eth开头就代表是传统的命名方式,eth0就是第一块网卡,以此类推。

那多队列网卡真正的作用又是什么呢?网卡接收数据会之后通过DMA将数据写入内存之后,会触发一个硬件中断告诉CPU数据在何处并且让CPU取数据,每一个中断都可以通过唤醒CPU让CPU核心上的内核线程单独处理数据,多队列网卡为每一个 RX 队列配备独立的 Ringbuffer 和独立的中断(从ens160的中断信息之中也可以看出来),从而实现真正的并行。多队列网卡的性能相比于一个队列性能会高很多原因在于单队列相当于死磕在一个核心,而多队列则让网络流分配到不用的队列之中让CPU核心并行处理提高吞吐,并且可以实现rx-0绑定到CPU 0,rx-1绑定到CPU 1,这样可以让两个CPU并行去各自的 Ringbuffer之中取出数据跑各自的协议。传统Linux内核处理模式依赖中断和多队列,可以通过下图看到传统网络数据流是如何从网卡到应用程序的。而DPDK直接把网卡多队列接管到用户态,关闭中断,4个vCPU分别对四个队列进行while(1)循环抓包,这样的多核并行能够真正做到零开销。

二、Huge Page

为了让CPU查找数据速度变快并且让网卡塞数据过程不被打断,需要设置好Huge Page,即大页内存。Linux 内存管理默认使用 4KB大小的页,Huge Page就是将内存页放大。

CPU访问内存时并不是直接拿着物理地址访问,而是经过 TLB (页表缓存)从物理地址映射虚拟地址的几十到几百个近期使用过的内存地址。而在网络数据流量巨大的情况下,TLB会被瞬间挤爆,CPU就需要去内存之中查询页表这个多级目录,一层一层往下查询物理地址会导致CPU算力浪费,这个过程也就是TLB Miss。频繁需要这个过程就会导致网络卡顿。Huge Page则可以用同样大小的内存,通过扩大单页尺寸减少页表项数量,同样多的内存地址映射下来可以用更少的页记录下相同的虚拟地址,降低TLB Miss。

Huge Page可以保证物理内存的连续,普通 4KB 内存页在物理上很可能是东一块、西一块,操作系统长久运行之后物理内存页碎片化严重。Huge Page会在系统刚启动的时候直接会在物理内存之中划走大块连续内存,锁死给大页专用。同时,普通的 4KB 页在分配和释放时,内核需要频繁修改页表,如果内存不够了还要触发换页,这会带来极大的延迟。大页因为数量少,分配后就一直锁在物理内存里,不会被操作系统换出,让 DPDK 的零拷贝和轮询模式不受任何打扰。总而言之大页使用更大的页面映射粒度,可能提高地址转换效率,但会带来内存管理上的取舍,内存回收和管理难度也会随之变大。

Huge Page主要支持 2MB和 1GB两种规格,DPDK默认使用2MB的大页,足够覆盖DMA需要的大块连续物理内存,降低TLB Miss发生的可能。1 GB规格主要是追求极致性能,系统启动前就需要预留,一旦分配就是彻底从操作系统手中拿走了这一块内存不能给其他的应用程序使用。终端输入:

grep -i huge /proc/meminfo

可以看到以下内容:

表明本虚拟机支持2MB规格的大页。更加推荐采用 2MB大页规格,从物理内存之中预留 1024 个 2MB 的大页,也就是 2GB 内存:

echo 1024 | sudo tee /proc/sys/vm/nr_hugepages

对于x86架构,1GB大页需要CPU有pdpe1gb标志,输入以下指令以后如果能看到pdpe1gb标志位表明当前系统也是支持1GB规格的大页。

cat /proc/cpuinfo | grep pdpe1gb

我本人采用永久修改巨页配置办法:

sudo vim /etc/default/grub # 修改 GRUB_CMDLINE_LINUX="find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US default_hugepagesz=2M hugepagesz=2M hugepages=1024" sudo update-grub sudo reboot #重启虚拟机

这样机器每一次启动之后第一件事就是去物理内存之中预留出2GB空间变成大页,不必每一次启动机器之后都需要echo一次。通过GRUB配置的2MB永久巨页对于我们后续的DPDK开发来说已经足够使用,当然你也可以把配置改成hugepagesz=1G hugepages=2这样的配置,注意在Linux系统配置之中遵循hugepagesz=<尺寸> hugepages=<数量>公式即可。

三、关于DPDK源码

可以在DPDK官网下载页得到源码,这里选择的是DPDK 19.08.2版本的DPDK源码。不同版本之间的源码存在些许差异,在学习初期推荐使用早期稳定版本,后续可以采用最新版本。

在虚拟机的设置之中可以看到下面三种网络连接方式:

桥接模式就是跟物理主机平级的方式,通过将虚拟网卡桥桥接到真实物理网卡之中让虚拟机像物理局域网之中的一台独立电脑。比如windows主机上面地址为192.168.0.123,桥接模式下路由器会通过DHCP给虚拟机分配一个跟主机同一网段的IP地址,即虚拟机可能地址为192.168.0.125。NAT模式则更像在windows系统里面开辟了一个子网,比如在我当前系统里面虚拟机分配地址是192.168.139.130,windows主机则在当前虚拟网络里面的地址是192.168.139.1。在NAT模式下,VMware Workstation软件自带的虚拟DHCP服务器会给虚拟机分配IP地址,虚拟机把数据报发送给默认网关192.168.139.2,本质上就是一个虚拟路由器,VMware 这个虚拟路由器收到包后,做了一次 NAT 转换,将虚拟机内网IP替换成为windows物理网卡真实IP,将转换的包给了底层系统的TCPIP协议栈并发送给真正的互联网。

在powershell提示下输入ipconfig得到VMware Network Adapter NMnet8网卡可以看到其对应的windows主机ip地址。注意此地址只是在本机虚拟子网里面使用的地址,真实上网的地址并不是此IP地址。

最终DPDK开发选择NAT模式或者桥接模式都可以,仅主机模式就无法与真实网络通信,可以暂时不考虑。

在DPDK源码目录下面执行sudo ./usertools/dpdk-setup.sh之后可以看到出现多个适应不同机器的环境编译选择。这个脚本出现在20.11版本以前,注意版本的使用。

目前我的虚拟机机器是x86_64架构,可以看到有36和39两种使用gcc的本地编译选择。如果需要对DPDK源码进行修改选择39,如果只使用DPDK提供的框架而不需要改变他的源码就选择36。

编译之前先下载libnuma开发工具包,避免编译器像下图这样找不到numa.h头文件。

sudo apt-get update sudo apt-get install libnuma-dev

Linux 5.6 起,net_device_ops.ndo_tx_timeout的回调原型从void (*)(struct net_device *)改成了void (*)(struct net_device *, unsigned int txqueue)。DPDK 19.08 的kni_net_tx_timeout仍是单参数,第.ndo_tx_timeout = kni_net_tx_timeout时类型不匹配,报-Wincompatible-pointer-types。kni_net_tx_timeout是DPDK 的KNI内核模块内部实现的回调函数,作用适用于KNI虚拟网卡发送数据超时卡住时内核网络子系统会调用他来处理异常情况。

在dpdk-stable-19.08.2/kernel/linux/kni/kni_net.c之中将kni_net_tx_timeout签名修改为以下内容

static void #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0) kni_net_tx_timeout(struct net_device *dev, unsigned int txqueue) #else kni_net_tx_timeout(struct net_device *dev) #endif { pr_debug("Transmit timeout at %ld, latency %ld\n", jiffies, jiffies - dev_trans_start(dev)); ​ dev->stats.tx_errors++; netif_wake_queue(dev); }

此外内核5.15使用了开了-Wimplicit-fallthrough=5,对于注释文本有比较严格的限制,在dpdk-stable-19.08.2/kernel/linux/igb_uio/igb_uio.c的case条件语句之中的/* falls through - to INTX */类似贯穿注释编译器不认可,需要改为如下的fallthrough;语句。以及在同目录下的Makefile文件之中加入MODULE_CFLAGS += -Wno-error=implicit-fallthrough保留警告当兵不当成错误。

static int igbuio_pci_enable_interrupts(struct rte_uio_pci_dev *udev) { ...... switch (igbuio_intr_mode_preferred) { case RTE_INTR_MODE_MSIX: /* Only 1 msi-x vector needed */ #ifndef HAVE_ALLOC_IRQ_VECTORS msix_entry.entry = 0; #else #endif ​ fallthrough; case RTE_INTR_MODE_MSI: #ifndef HAVE_ALLOC_IRQ_VECTORS ...... #endif fallthrough; case RTE_INTR_MODE_LEGACY: ...... fallthrough; case RTE_INTR_MODE_NONE: ...... ​ default: ...... ​ return err; }

GCC 10默认-fno-common,在旧规则-fcommon下,多个源文件里定义了同名的全局变量,链接器会容忍并把它们合并。但 GCC 10 默认-fno-common后,链接器会严格检查,发现多个同名全局变量就会直接报multiple definition of ...错误。在dpdk-stable-19.08.2/mk/toolchain/gcc/rte.vars.mk之中加上以下内容:

# GCC 10 and later default to -fno-common, while this DPDK version still # relies on tentative definitions being merged across translation units. ifeq ($(shell test $(GCC_MAJOR) -ge 10 && echo 1), 1) TOOLCHAIN_CFLAGS += -fcommon endif

编译完成之后会看到dpdk-stable-19.08.2/x86_64-native-linux-gcc产物,接下来依次选择43、44、45即uio和vfio模块,KNI接口。uio能让用户态程序直接读写网卡的寄存器并处理网卡的终端,vfio模块比uio更为强大,是基于输入输出内存管理单元硬件从而接管网卡的框架。uio和vfio模块都能够截获并且访问PCI的数据。

输入lsmod | grep igb_uio能看到已经准备就绪。

lazywin@ubuntu:~$ lsmod | grep igb_uio igb_uio 24576 0 uio 20480 1 igb_uio

输入ls -ls /dev/vfio能看到输出存在就说明VFIO框架已经存在

lazywin@ubuntu:~$ ls -l /dev/vfio/ total 0 crw-rw-rw- 1 root root 10, 196 Oct 25 18:21 vfio

46、47选项的NUMA表示非统一内存访问,以往统一内存访问就是一台电脑只有一个CPU,访问所有的内存条速度完全一样,现在服务器存在多核情况,主板上面存在多条内存,为了让避免让CPU A去访问较远的CPU B旁的内存,现代服务器被切分为了多个节点,每一个节点包含CPU以及其旁边的多条内存,如果CPU访问同一节点下面的内存条那么就是本地访问,访问其他节点的内存条是远程访问速度大幅降低。现代DPDK服务端开发的黄金法则就是网卡、CPU核心、大页内存必须绑定在同一个NUMA节点上面。

执行46和47操作并且page的数量我们都分配为512,也就是分配了512个2MB的大页,留出了1GB的绝对连续的物理内存给DPDK专门使用。

在大内存之下通常总内存由多条内存条组成,non-numa表示直接把大页内存丢入大池子之中不归属哪个CPU管理,numa则表示给每一个node分配多少内存,保证CPU只拿离自己近的本地内存,绝不跨节点。

接下来退出setup.sh脚本进入usertools的bash交互目录底下执行以下命令:

sudo python3 dpdk-devbind.py --status

输出以下内容:

这里看到了我最常用的ens160网卡,其中0000:03:00.0是网卡的 PCI 地址。当前采用的是VMXNET3型号的虚拟网卡,通过drv=vmxnet3可以看到目前这个网卡依然被 Linux 内核自带的vmxnet3驱动控制着,所以它还在作为普通网卡使用。unused=igb_uio,vfio-pci这说明 DPDK 里面的igb_uio和vfio-pci两个专用驱动已经加载到系统里了,但网卡还没被绑定过去。接下来我们就是要深入实现网卡从内核驱动程序解绑,然后绑定到DPDK用户态程序之中。

四、网卡解绑与绑定

如果你使用的远程工具诸如ssh、xshell、MobaXterm连接的虚拟机之后在主机进行的开发,在使用当前ens160网卡的PCI地址解除其绑定的vmxnet3内核驱动程序之前确保你建立了多个可以使用的虚拟网卡,避免取消了一个网卡绑定的内核驱动程序之后无法远程连接到主机上面使用或者正常上公网。

还有值得注意的一个细节是一张网卡其实唯一对应一个IP地址、一个MAC地址和PCI地址,远程连接工具是基于IP地址做的连接,确保解绑的网卡不是你用来专门做连接的网卡地址,将其他闲置的多队列虚拟网卡解绑献给DPDK的VFIO驱动。

很多初学者以为回环地址127.0.0.1也是网卡,其实它是个纯软件虚拟接口。真实的网卡对应的是 PCI 硬件,每新增一块物理/虚拟网卡,就相当于给服务器增加了一个独立的网络身份即IP地址。这也解释了为什么在 DPDK 开发中,我们可以把某一块网卡的 IP 直接抹掉,却不影响其他网卡上的 SSH 通信——因为网络身份的隔离,本质上就是网卡硬件的隔离。

接下来我们需要卸载掉一张网卡上面对应的vmxnet3的内核驱动,注意vmxnet3作为一个内核模块并不会因为一个网卡卸载了对其的内核驱动依赖就会瘫痪,其只要存在仍然可以正常运行工作,继续管理其他绑定在他身上的网卡。

接下来我们在菜单之中选择49将网卡设备绑定到igb_uio模块,首先我们需要获取我们需要从内核驱动程序之中解绑网卡的PCI地址。这里选择将ens192解绑,执行:

ethtool -i ens192 | grep bus-info

得到ens192的PCI地址为0000:0b:00.0,先执行sudo ifconfig ens192 down将其down掉,先暂停这张网卡的服务。然后选择49再输入ens192的PCI地址可以看到现在已经将ens192网卡绑定到了uio模块。

此时在输入ip addr就看不到ens192这一块网卡:

也可以通过当前网卡状态看到DPDK的VFIO驱动接管了ens192网卡:

之后可以将example底下某个示例的Makefile文件文件拷贝到工作目录下面,更改其中的binary name和SRCS-y为工作目录名称,直接在根目录底下就可以make自身基于DPDK开发的诸如高性能防火墙或用户态协议栈。

参考内容:

DPDK开发参考手册

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询