1. 轮询模式
1.1 为什么要放弃中断?
传统网卡驱动工作在内核态,采用的是异步中断模式。
当网卡接收到一个数据包:
- 网卡会产生硬件中断(MSI-X / MSI / INTX);
- CPU 响应中断,执行中断服务程序(ISR);
- 中断服务程序读取网卡收包队列,完成包的拷贝与处理。
这个过程虽然可靠,但问题在于每一次中断都意味着:
- 上下文切换(kernel ↔ user);
- CPU pipeline 被打断;
- Cache 命中率下降。
在低速或低流量场景下,这没问题;
但在高速万兆或百兆包转发下,中断频率可能高达百万次每秒,CPU 根本“喘不过气”。
举个简单的估算:
假设每次中断+上下文切换开销是 2 微秒,
当每秒收 1 百万个包,就会浪费 2 秒的 CPU 时间——比 1 个核心还多!
1.2 DPDK 的答案:纯轮询模式
DPDK 的 Poll Mode Driver(PMD)采用了彻底去中断化的思路。
- 不再等待网卡中断;
- CPU 核心通过轮询不断查询网卡队列;
- 发现有包则直接取走处理,没有包就继续轮询。
这种模式看似“暴力”,但却能带来极致的低延迟与高吞吐。
理解它的关键:CPU 不再被动等待,而是主动问网卡“有包吗?”。
1.3 收包过程简化示意
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 驱动初始化 RX 队列 | 分配描述符数组,每个指向 mbuf 缓冲区 |
| 2 | 网卡写入数据 | 收到包后 DMA 到对应缓冲区 |
| 3 | 驱动轮询队列 | 检查描述符中的“收包成功标志” |
| 4 | 标志置位 → 驱动取包 | 解析描述符、封装 mbuf |
| 5 | 回填新的 mbuf | 准备下一轮收包 |
因为整个过程中没有中断、没有系统调用、没有内核切换, 所以每个核心都能专心处理网络数据——这正是 DPDK 能够达到数千万 PPS 的秘诀。
1.4 发包过程:同样基于轮询
发包过程也一样:
- 应用准备好要发送的 mbuf;
- 驱动根据包内容填充 TX 描述符;
- 网卡根据描述符进行 DMA 传输;
- 驱动轮询硬件写回标志,判断发包是否完成;
- 回收已发送的描述符与内存块。
两个关键标志:
| 标志 | 作用 |
|---|---|
| EOP (End of Packet) | 表示一个完整包的结束 |
| RS (Report Status) | 告诉网卡哪些包需要上报发送状态 |
部分网卡可以配置每 N 个包报告一次状态,进一步降低写回频率,提高性能。
1.5 混合中断轮询模式:兼顾性能与节能
纯轮询的缺点也很明显——即使没有任何包,CPU 也要满速查询,白白浪费计算资源。
所以 DPDK 开始引入了 Hybrid Polling Mode(中断+轮询混合模式) 。
其核心思路与 Linux NAPI 类似:
- 当流量高时:持续轮询,追求高性能;
- 当流量低时:转为中断触发,CPU 可以休眠或处理其他任务。
实例:l3fwd-power 的策略
-
应用开始时采用轮询;
-
当连续多次发现无包可收时:
- 启用收包中断;
- 线程休眠;
-
有包进入时触发中断;
-
被唤醒后关闭中断,重新进入轮询状态。
这套机制让 DPDK 应用在高负载高性能与低负载低功耗之间智能切换。
1.6 UIO 与 VFIO 的中断机制差异
DPDK 的混合中断机制基于两种驱动框架:
| 模式 | 特点 |
|---|---|
| VFIO | 支持多 MSI-X 中断,可为每个 RX 队列分配独立中断 |
| UIO | 只支持单个中断号,所有队列共享 |
一般来说,VFIO 更现代、更灵活,推荐在实际部署中优先使用。
1.7 性能与延迟的权衡
混合中断模式的代价是:
- 唤醒延迟增加(第一个包可能被延迟处理);
- 稍高的 jitter(时延抖动);
- 需要适当调整 mbuf ring 大小以防突发丢包。
但在流量不稳定、节能要求高的场景(如边缘计算、IoT 网关)非常实用。
1.8 总结:三种模式一图看懂
| 模式 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|
| 异步中断模式 | CPU 空闲可用于其他任务 | 上下文切换开销大 | 普通内核态网络 |
| 轮询模式 | 极致性能、低延迟 | CPU 占用高 | 高频金融交易、NFV |
| 混合模式 | 节能与性能平衡 | 唤醒延迟略高 | 动态流量、边缘网关 |
2. 为什么要关注“网卡性能优化”?
在高性能网络系统中,比如用户态网络栈、DPDK、SDN vSwitch 等,网卡收发包性能往往是决定整个系统上限的关键因素。优化网络性能,不仅仅是“让网卡更快”,而是要减少 CPU 的等待时间(时延) 、提高并行执行能力(吞吐) 、避免缓存竞争。 换句话说,我们优化的目标是:让 CPU 在同样时间内做更多有用的工作。
2.1 CPU 微架构的两把尺子:时延与吞吐
基本定义
- 时延(Latency) :一条指令从发出到执行完成所需的时钟周期数。
- 吞吐(Throughput) :同类型指令再次发射前所需等待的时钟周期数。
通俗来说:
- 时延描述“你要等多久”;
- 吞吐描述“你能多快地并行干更多活”。
例如,一条 load 指令可能时延为 4 个周期,但吞吐为 1 个周期。 这意味着虽然每条加载都要花 4 个周期,但 CPU 每个周期都能再发出一条新的加载指令——这就是“时延隐藏” 。
2.2 时延隐藏与批量处理的威力
我们来看一个简化示例:
| 指令 | 含义 | 时延 | 吞吐 |
|---|---|---|---|
| Load | 取数据 | 4 | 1 |
| ALU | 计算 | 1 | 1 |
| Store | 写回 | 1 | 1 |
如果顺序执行 4 次事务(Load → ALU → Store),需要 28 个周期。 但若将多个事务同时展开(例如 4 路并发 Load),CPU 可以在等待一次加载完成时继续发出其他事务的加载指令,从而将总耗时缩短至 16 周期左右。
这就是 通过批处理实现时延隐藏 —— 并行多发,掩盖等待。
2.3 在 DPDK 中的应用:收包的批处理优化(Bulk Receive)
在 DPDK 的 ixgbe 驱动中,收包函数就是这种思想的典型实践。
传统逐包收取的流程如下:
- 检查 DD 标志(DMA 是否完成)
- 解析包描述符
- 分配 Mbuf
- 重填描述符
- 更新尾指针
这个流程里,大量 Load/Store 操作访问内存中的环形缓冲区,非常依赖缓存命中率。
批处理(Bulk)优化思路
改造后,DPDK 会:
- 每次扫描收包环时,看是否有一批(8~32个)包可以收;
- 批量解析描述符;
- 批量分配 Mbuf;
- 批量重填描述符;
- 最后统一更新尾指针(减少 MMIO 写操作次数)。
这样可以显著降低内存访问与缓存一致性同步的成本。 CPU 与 DMA 不再频繁竞争同一个 cache line 的写权限,极大提升吞吐。
DPDK 对应代码中的限制条件(ixgbe_recv_pkts_bulk_alloc()):
/*
* Make sure the following pre-conditions are satisfied:
* rxq->rx_free_thresh >= RTE_PMD_IXGBE_RX_MAX_BURST
* rxq->rx_free_thresh < rxq->nb_rx_desc
* (rxq->nb_rx_desc % rxq->rx_free_thresh) == 0
* rxq->nb_rx_desc < (IXGBE_MAX_RING_DESC - RTE_PMD_IXGBE_RX_MAX_BURST)
* Scattered packets are not supported.
*/
这些条件保证批处理的正确性与环形缓冲区对齐,防止越界和缓存写竞争。
2.4 再进一步:利用 SIMD 指令挖掘 CPU 带宽潜力
即使批处理之后,CPU 的访存带宽仍未完全利用。
Intel CPU 的 L1 缓存每周期可支持:
- 32B 读取 + 16B 写入(Sandy Bridge)
- 64B 读取 + 32B 写入(Haswell)
但普通 64 位 load 指令一次只加载 8B 数据。 哪怕双发射,每周期也只能加载 16B——只用了 1/4 的带宽!
解决方案:SIMD(向量化指令)
- SSE:128bit(16B)
- AVX:256bit(32B)
例如,两路 AVX load 可以在一个时钟周期读取 64B 数据,完全吃满 Haswell 的带宽。
这对 DPDK 的包解析、描述符处理等密集内存操作场景,提升巨大。
但也有挑战:
- SIMD 操作要求数据结构严格对齐;
- DPDK Mbuf、描述符字段需要精心布局;
- 数据需要通过
shuffle、模板化操作做轻量格式转换。
DPDK 中的向量化 PMD(vector PMD)正是基于此设计的。 为了性能,会牺牲一些高级特性(如 scatter/gather、offload),但获得更高的包吞吐能力。
3. PCIe:网卡性能的“血管系统”
DPDK 性能的第一道关口就是 PCIe总线。 它是网卡与CPU之间传输数据的主要通道,速率、Lane数量、版本号都会直接决定网络带宽上限。
以 Intel XL710-40G 网卡为例:
- 支持 PCIe Gen3 ×8;
- 若插入 Gen2 槽位,则会降级传输;
- 结果:系统总带宽不足,40G 网卡性能上不去。
正确做法:
通过命令 lspci -vvv 检查实际的速率与Lane:
LnkSta: Speed 8GT/s, Width x8, ...
表示该设备工作在 Gen3 ×8 模式下,带宽充足。
3.1 Extended Tag:被忽视的性能开关
PCIe 规范中定义了一个重要特性 —— Extended Tag。 它决定了设备可以同时发出的并发请求数量。
| 模式 | 并发请求数 | 说明 |
|---|---|---|
| 默认关闭 | 32 | 仅5位Tag有效 |
| 启用(8位) | 256 | 并发提升8倍 |
在高速网卡(40G / 100G)场景中,若未启用该特性,PCIe可能成为瓶颈。
三种启用方式
-
BIOS 中显式 Enable;
-
Linux 下使用
setpci; -
修改 DPDK 编译配置:
CONFIG_RTE_PCI_CONFIG=y CONFIG_RTE_PCI_EXTENDED_TAG="on"
3.2 理解 NUMA:双路服务器的性能陷阱
在典型的双路服务器上,PCIe 槽位会直接连接到某个 CPU 节点(NUMA node)。 如果网卡插在 Node 1 上,却绑定到 Node 0 的核心处理数据,会出现 跨NUMA访问,带宽下降、延迟上升。
检查 CPU 与 NUMA 对应关系
lscpu
示例输出:
NUMA node0 CPU(s): 0-17,36-53
NUMA node1 CPU(s): 18-35,54-71
检查网卡属于哪个NUMA节点
lspci -nn | grep Eth
# 例如:
# 82:00.0 Ethernet controller [0200]: Intel XL710 for 40GbE QSFP+
结合 lstopo 或 /sys/bus/pci/devices/0000:82:00.0/numa_node 即可确定该网卡属于哪一个 CPU 节点。
DPDK最佳实践:
- 网卡端口与线程绑定在同一 NUMA node;
- 避免跨 Socket 访问(跨 CPU 内存)。
3.3 BIOS与内核参数:让CPU全速运转
默认服务器BIOS倾向于节能模式,这对 DPDK 性能是灾难。
BIOS调优建议
- 关闭 C-State、P-State(节能模式);
- 开启 Turbo Boost;
- 设置内存运行在最高频率;
- 确认 PCIe 代际与 Lane 匹配。
Linux 启动参数建议
default_hugepagesz=1G hugepagesz=1G hugepages=8
isolcpus=2,3,4,5,6,7,8
- 前者配置 8 个 1GB Hugepage;
- 后者隔离 CPU 核心给 DPDK 使用。
检查是否生效:
cat /proc/meminfo | grep Huge
输出:
HugePages_Total: 8
Hugepagesize: 1048576 kB
3.4 队列、核绑定与RSS均衡:DPDK多核并行的关键
高速网卡(如 XL710)通常单队列无法达到线速。
解决方案是启用多个 RX/TX 队列 + RSS(Receive Side Scaling)。
DPDK 示例(L3FWD)
假设有两个 XL710 网卡:
- PCIe 地址分别为
0000:82:00.0与0000:85:00.0 - 每端口 4 个队列
- 使用 Node1 上的 8 个核(ID 20–27)
运行命令如下:
./l3fwd -c 0xff00000 -n 4 \
-w 82:00.0 -w 85:00.0 -- \
-p 0x3 -config="(0,0,20),(0,1,21),(0,2,22),(0,3,23),(1,0,24),(1,1,25),(1,2,26),(1,3,27)"
参数说明:
(port_id, queue_id, core_id)
每个队列绑定一个核心,最大化并行处理能力。
⚠️ 若 RSS 未均匀分配流量(例如大部分包进入单一队列),性能会大幅下降。
4. 为什么要关注“队列与阈值”?
很多人做 DPDK 性能优化时,会首先想到:
- 是否 NUMA 绑定正确?
- 是否使用了大页内存?
- 是否 CPU 亲和绑定合理?
但在这些都做完之后,依然可能遇到:
- 小包丢包;
- 发包不满线速;
- 性能波动大。
而这些问题,往往都藏在网卡队列和阈值设置里。 这些参数控制着包在内存与网卡之间的“流速阀门”,调整得当,性能可提升 10%~30%。
4.1 收发队列的本质:缓存与流量平衡
DPDK 的收发过程可以简单类比成流水线:
- 收包队列(RX Queue) :是“流入缓冲池”,决定能同时缓存多少未被应用取走的包;
- 发包队列(TX Queue) :是“流出缓冲池”,决定能暂存多少待发送的数据。
这两个参数看似简单,其实是 DPDK 稳定与性能的平衡点。
4.2 收包队列长度(RX Descriptor Length)
参数定义
收包队列长度指每个 RX 队列中描述符(descriptor)的数量,每个描述符对应一个 Mbuf 缓冲区。
- 队列越长 → 能缓存更多包,不易丢包;
- 队列越短 → 内存开销小,延迟低。
默认值与调整建议
| 场景 | 建议队列长度 | 说明 |
|---|---|---|
| 普通千兆/10G 网卡 | 128(默认) | 通用配置,低延迟优先 |
| 高速 25G/40G 环境 | 512 | 增强收包缓冲能力 |
| 极高速/压力测试 | 1024 | 保证峰值时不丢包 |
实际中发现丢包率高、CPU 未满载时,优先尝试增加 RX 队列长度,往往立竿见影。
4.3 发包队列长度(TX Descriptor Length)
参数定义
每个 TX 队列的描述符数量,代表能暂存多少待发送的包。
- 队列长 → 能批量发送,效率高;
- 队列短 → 响应快,但容易“空转”。
默认值与调整建议
| 场景 | 建议队列长度 |
|---|---|
| 常规应用 | 512(默认) |
| 高速转发 | 1024 |
| 延迟敏感 | 256~512 |
一个典型现象是:TX 队列过短时,网卡空闲等待驱动填充新包,导致发包速率掉到 80~90% 线速。
4.4 三个关键阈值参数
这三个参数控制了 DPDK 驱动与网卡交互的“刷新节奏”。
4.4.1 收包释放阈值(rx_free_thresh)
- 定义:当释放的 RX 描述符数达到该阈值后,驱动才更新硬件队列尾部寄存器;
- 作用:减少寄存器写操作,提高性能;
- 默认值:32。
调优思路:
- 如果队列较长,可适当加大(如 64);
- 如果延迟敏感,保持默认或更小(如 16)。
4.4.2 发包结果报告阈值(tx_rs_thresh)
- 定义:设置网卡在发送完多少包后,才报告一次“发送完成”;
- 默认值:32;
- 值越小 → 回写频繁,CPU 开销大;
- 值越大 → 回写减少,性能好,但释放延迟可能增大。
Intel XL710 网卡测试中,
tx_rs_thresh=64通常可提升 5% 左右发包效率。
4.4.3 发包描述符释放阈值(tx_free_thresh)
- 定义:控制驱动在积累到一定数量可释放描述符后,才统一释放;
- 默认值:32;
- 太小 → 频繁释放影响 cache;
- 太大 → Mbuf 堆积占用内存。
推荐与
tx_rs_thresh保持接近,一般tx_free_thresh = tx_rs_thresh / 2或tx_rs_thresh效果最佳。