mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
6696 字
19 分钟
DPDK 轮询模式驱动
2025-04-25

某运营商的 DPI 系统每秒需要处理 2000 万个数据包,但传统中断驱动的网卡在流量洪峰时中断风暴导致 CPU 几乎无法处理任何正常任务。切换到轮询模式后,同样硬件的处理能力提升了 4 倍。轮询模式驱动(PMD)是 DPDK 性能的基础引擎。

PMD(Poll Mode Driver,轮询模式驱动)是 DPDK 的心脏。在第 3 章中介绍了 DPDK 的整体架构,在第 4 章中我们剖析了 DPDK 的内存管理——大页、mempool、mbuf。现在,我们终于来到了数据包真正流动的地方:网卡如何把包交给用户态程序?用户态程序如何把包送回网卡?

答案就是 PMD。传统内核网卡驱动依赖中断通知 CPU 有新包到达,而 PMD 反其道而行——它让 CPU 不断轮询网卡的接收描述符环,一旦发现新包就立即处理。这种”忙等”看似浪费 CPU,但在高吞吐场景下,它消除了中断处理、上下文切换、协议栈解析的全部开销,将单核收包性能从内核态的 12 Mpps 推升到 4080 Mpps。

本章将深入 PMD 的每一个层面:轮询与中断的设计哲学、rte_ethdev 抽象层 API、RX/TX 队列与批量操作、VFIO/UIO 设备访问路径、SR-IOV 虚拟功能、Bond 链路聚合,以及 rte_flow 硬件卸载。理解了这些,你就掌握了用户态网卡编程的完整技术栈。

一、轮询 vs 中断:PMD 的设计哲学#

1.1 中断驱动模型#

传统内核网卡驱动采用中断驱动模型:网卡收到数据包后,向 CPU 发送硬件中断,CPU 响应中断后执行中断处理程序(Top Half),将数据包从网卡 DMA 缓冲区拷贝到 sk_buff,然后触发软中断(Bottom Half)完成协议栈处理。在第 1 章中详细分析过这条路径的开销。

中断驱动模型在低包速率场景下表现良好——CPU 可以在空闲时进入低功耗状态,只在有包到达时才被唤醒。但当包速率升高时,问题就出现了:

  • 中断风暴:每秒百万级的中断请求,CPU 大量时间花在中断上下文切换上
  • 中断节流(Interrupt Throttling):内核通过 NAPI 机制将中断模式转为轮询模式,但这是在内核态轮询,仍然要穿越协议栈
  • 缓存失效:频繁的中断导致 CPU 缓存行不断被替换,数据局部性被破坏
  • 锁竞争:中断上下文与进程上下文并发访问共享数据结构,需要加锁保护

1.2 轮询驱动模型#

PMD 的核心思想极其简单:不等待通知,主动去查。CPU 绑定到一个 lcore 线程上,不断调用 rte_eth_rx_burst() 查询网卡是否有新包到达。如果有,立即处理;如果没有,下一轮继续查。

轮询模型的优势:

特性中断模型轮询模型(PMD)
CPU 利用率空闲时低,高负载时中断开销大始终 100%(一个核被轮询占用)
延迟中断响应延迟(微秒级)轮询周期内即时处理(纳秒级)
吞吐量受中断处理能力限制仅受硬件和内存带宽限制
功耗空闲时低始终高功耗
适用场景通用网络、低包速率高吞吐、低延迟数据平面
Note

轮询模型”浪费”的 CPU 并非真的浪费——在数据平面场景中,这个核本来就要处理大量数据包,轮询只是让它始终处于”准备就绪”状态。如果系统有足够的 CPU 核心,用一个核专职轮询收包是性价比极高的选择。

1.3 混合模式:中断 + 轮询#

纯粹的轮询模型在低流量场景下确实浪费电力。DPDK 从 22.11 版本开始支持混合中断/轮询模式(Hybrid Polling),允许在低流量时切换到中断模式,高流量时切换回轮询模式:

// 启用 RX 中断(DPDK 22.11+)
// 将特定队列设置为中断模式
rte_eth_dev_rx_intr_enable(port_id, queue_id);
// 在 epoll 中等待中断
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = rx_intr_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, rx_intr_fd, &ev);
// 中断触发后,切换回轮询模式处理数据包
int n = epoll_wait(epfd, &ev, 1, timeout);
if (n > 0) {
rte_eth_dev_rx_intr_disable(port_id, queue_id);
// 进入轮询模式批量收包
while (1) {
nb_rx = rte_eth_rx_burst(port_id, queue_id, bufs, BURST_SIZE);
if (nb_rx == 0) break;
// 处理数据包...
}
rte_eth_dev_rx_intr_enable(port_id, queue_id);
}

混合模式的核心思想是自适应:流量低时用中断省电,流量高时用轮询保性能。这在边缘计算和 IoT 网关等场景中特别有价值——流量波动大,既需要高峰期的线速处理能力,又需要低谷期的低功耗运行。

1.4 两种模型的处理流程对比#

flowchart TB subgraph 中断模型["中断驱动模型"] direction TB I1["网卡收到数据包"] --> I2["向 CPU 发送硬件中断"] I2 --> I3["CPU 保存上下文,跳转中断向量"] I3 --> I4["Top Half: 禁用中断,NAPI 调度"] I4 --> I5["Bottom Half: softirq 处理"] I5 --> I6["sk_buff 分配与拷贝"] I6 --> I7["协议栈逐层解析"] I7 --> I8["数据放入 socket 队列"] I8 --> I9["唤醒用户进程 recv()"] end subgraph 轮询模型["PMD 轮询模型"] direction TB P1["lcore 线程循环调用 rx_burst()"] --> P2{"描述符环有新包?"} P2 -->|是| P3["DMA 已完成,mbuf 直接可用"] P3 --> P4["用户态直接处理数据包"] P4 --> P1 P2 -->|否| P1 end style 中断模型 fill:#fff3e0,stroke:#e65100 style 轮询模型 fill:#e8f5e9,stroke:#2e7d32
Warning

轮询模型要求 CPU 核心被独占使用。如果操作系统调度器将其他任务调度到该核心上,轮询线程会被抢占,导致收包延迟抖动。因此,DPDK 应用必须通过 EAL 参数 -l-c 将 lcore 绑定到隔离的 CPU 核心上,并在内核启动参数中用 isolcpus 隔离这些核心。

二、rte_ethdev:以太网设备抽象层#

2.1 设备生命周期#

DPDK 通过 rte_ethdev 层抽象所有以太网设备,无论是物理网卡、虚拟功能(VF)还是虚拟设备(如 pcap、ring),都通过统一的 API 操作。设备生命周期遵循严格的状态机:

stateDiagram-v2 [*] --> Probe: EAL 发现设备 Probe --> Uninitialized: 驱动 probe 成功 Uninitialized --> Configured: rte_eth_dev_configure() Configured --> Started: rte_eth_dev_start() Started --> Started: 收发包<br/>rx_burst/tx_burst Started --> Configured: rte_eth_dev_stop() Configured --> Uninitialized: rte_eth_dev_close() Uninitialized --> [*]: 设备释放

每个状态转换都有对应的 API,且必须按顺序调用——你不能在未 configure 的情况下 start,也不能在未 stop 的情况下 close。这保证了设备状态的一致性。

2.2 核心配置 API#

rte_eth_dev_configure:配置设备的全局参数

#include <rte_ethdev.h>
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
int port_init(uint16_t port, struct rte_mempool *mbuf_pool)
{
struct rte_eth_conf port_conf = {
.rxmode = {
.max_rx_pkt_len = RTE_ETHER_MAX_LEN,
.offloads = 0,
},
.txmode = {
.offloads = 0,
},
};
struct rte_eth_dev_info dev_info;
int ret;
// 获取设备信息(能力、支持的 offload 等)
ret = rte_eth_dev_info_get(port, &dev_info);
if (ret != 0)
return ret;
// 配置设备:1 个 RX 队列 + 1 个 TX 队列
ret = rte_eth_dev_configure(port, 1, 1, &port_conf);
if (ret != 0)
return ret;
// ... 后续队列配置
return 0;
}

rte_eth_dev_configure() 的关键参数:

参数说明
nb_rx_q接收队列数量,通常与 RSS 哈希队列数一致
nb_tx_q发送队列数量,通常与 lcore 数量一致
rxmode.offloads接收侧硬件卸载标志(如 VLAN 剥离、校验和等)
txmode.offloads发送侧硬件卸载标志(如 TSO、校验和插入等)
Note

offloads 字段决定了哪些工作由网卡硬件完成、哪些由软件完成。合理配置 offload 可以显著降低 CPU 负载。例如,启用 DEV_RX_OFFLOAD_CHECKSUM 后,网卡会在接收描述符中标记校验和是否正确,应用无需再软件计算。

2.3 队列配置 API#

rte_eth_rx_queue_setup:配置接收队列

// 配置 RX 队列
ret = rte_eth_rx_queue_setup(port, 0, RX_RING_SIZE,
rte_eth_dev_socket_id(port), NULL, mbuf_pool);
if (ret < 0)
rte_exit(EXIT_FAILURE, "rte_eth_rx_queue_setup: err=%d\n", ret);

rte_eth_tx_queue_setup:配置发送队列

// 配置 TX 队列
ret = rte_eth_tx_queue_setup(port, 0, TX_RING_SIZE,
rte_eth_dev_socket_id(port), NULL);
if (ret < 0)
rte_exit(EXIT_FAILURE, "rte_eth_tx_queue_setup: err=%d\n", ret);

队列配置的关键参数:

参数说明典型值
queue_id队列编号,从 0 开始0 ~ nb_q-1
nb_rx_desc/nb_tx_desc描述符环大小512、1024、4096
socket_idNUMA 节点 ID,影响内存分配位置rte_eth_dev_socket_id()
rx_conf/tx_conf队列级配置(阈值、offload 覆盖等)NULL 使用默认值
mb_poolRX 队列的 mbuf 内存池必须与端口在同一 NUMA 节点

2.4 收发包核心 API#

rte_eth_rx_burst:批量接收数据包——这是 PMD 最核心的 API

// 从 port_id 的 queue_id 批量接收最多 BURST_SIZE 个包
struct rte_mbuf *bufs[BURST_SIZE];
uint16_t nb_rx;
nb_rx = rte_eth_rx_burst(port_id, queue_id, bufs, BURST_SIZE);
// 处理接收到的包
for (i = 0; i < nb_rx; i++) {
// bufs[i] 即为一个完整的以太网帧
struct rte_ether_hdr *eth = rte_pktmbuf_mtod(bufs[i], struct rte_ether_hdr *);
// ... 业务逻辑处理
}
// 处理完毕后释放 mbuf
for (i = 0; i < nb_rx; i++)
rte_pktmbuf_free(bufs[i]);

rte_eth_tx_burst:批量发送数据包

// 批量发送数据包
struct rte_mbuf *tx_bufs[BURST_SIZE];
uint16_t nb_tx;
// 准备要发送的 mbuf(从 mempool 分配并填充数据)
for (i = 0; i < nb_pkts_to_send; i++) {
tx_bufs[i] = rte_pktmbuf_alloc(mbuf_pool);
// 填充以太网帧数据...
}
nb_tx = rte_eth_tx_burst(port_id, queue_id, tx_bufs, nb_pkts_to_send);
// 发送未成功的包需要释放
if (unlikely(nb_tx < nb_pkts_to_send)) {
for (i = nb_tx; i < nb_pkts_to_send; i++)
rte_pktmbuf_free(tx_bufs[i]);
}
Warning

rte_eth_tx_burst() 不保证所有包都能立即发送——TX 描述符环可能已满。未发送的 mbuf 必须由应用负责释放,否则会导致内存泄漏。在第 4 章中讨论过 mbuf 的生命周期管理,这里再次强调:谁分配,谁释放

2.5 完整的设备初始化序列#

将上述 API 组合起来,一个完整的 DPDK 以太网设备初始化流程如下:

#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>
#define RX_RING_SIZE 1024
#define TX_RING_SIZE 1024
#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
static const struct rte_eth_conf port_conf_default = {
.rxmode = {
.max_rx_pkt_len = RTE_ETHER_MAX_LEN,
},
};
/* 初始化端口 */
static inline int
port_init(uint16_t port, struct rte_mempool *mbuf_pool)
{
struct rte_eth_conf port_conf = port_conf_default;
const uint16_t rx_rings = 1, tx_rings = 1;
uint16_t nb_rxd = RX_RING_SIZE;
uint16_t nb_txd = TX_RING_SIZE;
int retval;
uint16_t q;
struct rte_eth_dev_info dev_info;
struct rte_eth_txconf txconf;
if (!rte_eth_dev_is_valid_port(port))
return -1;
retval = rte_eth_dev_info_get(port, &dev_info);
if (retval != 0) {
printf("Error during getting device (port %u) info: %s\n",
port, strerror(-retval));
return retval;
}
// 配置以太网设备
retval = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
if (retval != 0)
return retval;
// 调整描述符数量为设备支持的合法值
retval = rte_eth_dev_adjust_nb_rx_tx_desc(port, &nb_rxd, &nb_txd);
if (retval != 0)
return retval;
// 配置每个 RX 队列
for (q = 0; q < rx_rings; q++) {
retval = rte_eth_rx_queue_setup(port, q, nb_rxd,
rte_eth_dev_socket_id(port), NULL, mbuf_pool);
if (retval < 0)
return retval;
}
// 配置每个 TX 队列
txconf = dev_info.default_txconf;
txconf.offloads = port_conf.txmode.offloads;
for (q = 0; q < tx_rings; q++) {
retval = rte_eth_tx_queue_setup(port, q, nb_txd,
rte_eth_dev_socket_id(port), &txconf);
if (retval < 0)
return retval;
}
// 启动设备
retval = rte_eth_dev_start(port);
if (retval < 0)
return retval;
// 启用混杂模式(可选,根据需求)
retval = rte_eth_promiscuous_enable(port);
if (retval != 0)
return retval;
return 0;
}

这个初始化序列是几乎所有 DPDK 应用的起点——从 rte_eth_dev_configurerte_eth_dev_start,每一步都不可或缺。rte_eth_dev_adjust_nb_rx_tx_desc 是一个容易被忽略但很重要的函数:它会将你请求的描述符数量调整为硬件支持的最接近合法值,避免因非法参数导致配置失败。

三、RX/TX 队列与批量操作#

3.1 RX 描述符环#

接收队列的核心是一个描述符环(Descriptor Ring)——一块由驱动分配、网卡通过 DMA 访问的内存区域。每个描述符指向一个 mbuf,网卡将收到的数据包 DMA 到该 mbuf 中,然后更新描述符的状态标志。

flowchart LR subgraph 主机内存["主机内存"] subgraph 描述符环["RX 描述符环 (环形缓冲区)"] D0["Desc 0<br/>→ mbuf A (已用)"] D1["Desc 1<br/>→ mbuf B (已用)"] D2["Desc 2<br/>→ mbuf C (空闲)"] D3["Desc 3<br/>→ mbuf D (空闲)"] D4["Desc 4<br/>→ mbuf E (空闲)"] D5["..."] end subgraph Mbuf池["Mbuf 内存池"] MA["mbuf A<br/>含数据包"] MB["mbuf B<br/>含数据包"] MC["mbuf C<br/>空(等待DMA)"] MD["mbuf D<br/>空(等待DMA)"] end end subgraph 网卡["网卡硬件"] NIC["NIC DMA Engine"] RDT["RDT 寄存器<br/>(软件写指针)"] RDH["RDH 寄存器<br/>(硬件写指针)"] end NIC -->|"DMA 写入数据包"| MC NIC -->|"DMA 写入数据包"| MD RDT -->|"指向下一个<br/>可用描述符"| D2 RDH -->|"指向下一个<br/>待写入描述符"| D2 style 描述符环 fill:#e3f2fd,stroke:#1565c0 style Mbuf池 fill:#e8f5e9,stroke:#2e7d32 style 网卡 fill:#fff3e0,stroke:#e65100

RX 描述符环的工作流程:

  1. 初始化:驱动为每个描述符分配一个 mbuf,将 mbuf 的物理地址写入描述符,网卡知道数据包应该 DMA 到哪里
  2. 网卡收包:网卡收到数据包后,通过 DMA 将数据写入当前描述符指向的 mbuf,然后更新 RDH(Receive Descriptor Head)寄存器
  3. 软件轮询:PMD 不断检查描述符的 DD(Descriptor Done)位,如果为 1,说明该描述符对应的数据包已由网卡 DMA 完成
  4. 软件处理:PMD 取出 mbuf 处理数据包,然后分配新的 mbuf 重新填充描述符,更新 RDT(Receive Descriptor Tail)寄存器通知网卡有新的空闲描述符可用
Note

RDH 和 RDT 两个寄存器构成了生产者-消费者模型:网卡是生产者(移动 RDH),软件是消费者(移动 RDT)。两者之间的距离就是当前可用的描述符数量。如果 RDH 追上 RDT,说明描述符环已满,网卡不得不丢包。

3.2 TX 描述符环#

发送队列同样使用描述符环,但方向相反:软件是生产者,网卡是消费者。

  1. 软件发送:应用调用 rte_eth_tx_burst(),将待发送的 mbuf 填入 TX 描述符,更新 TDT(Transmit Descriptor Tail)寄存器通知网卡有新包待发
  2. 网卡发送:网卡 DMA 读取描述符指向的 mbuf 数据,通过网络发送出去,然后更新 TDH(Transmit Descriptor Head)寄存器
  3. 软件回收:PMD 在下次发送前检查已发送描述符的 DD 位,如果为 1,说明网卡已完成发送,可以回收对应的 mbuf
// TX 描述符环的回收逻辑(简化版)
// 在每次 tx_burst 之前,检查已发送的描述符并释放 mbuf
static inline void
tx_free_bufs(struct tx_queue *txq)
{
uint16_t nb_tx = txq->nb_tx;
uint16_t nb_free = 0;
// 检查已发送描述符的 DD 位
while (nb_free < txq->free_thresh &&
(txq->tx_desc[nb_tx].status & DD_BIT)) {
// 释放已发送的 mbuf
rte_pktmbuf_free(txq->tx_bufs[nb_tx]);
txq->tx_bufs[nb_tx] = NULL;
nb_tx = (nb_tx + 1) % txq->nb_desc;
nb_free++;
}
txq->nb_tx = nb_tx;
}

3.3 批量操作:为什么 burst 至关重要#

rte_eth_rx_burst()rte_eth_tx_burst()burst 参数指定了一次性收发多少个包。典型值为 32,最大为 512。批量操作对性能的影响是决定性的:

1. 缓存预取(Cache Prefetch)

批量收包后,PMD 可以对即将处理的 mbuf 进行预取,让数据在 CPU 需要时已经位于 L1/L2 缓存中:

// 批量收包 + 预取优化
nb_rx = rte_eth_rx_burst(port_id, queue_id, bufs, BURST_SIZE);
for (i = 0; i < nb_rx; i++) {
// 预取下一个 mbuf 的数据到缓存
rte_prefetch0(rte_pktmbuf_mtod(bufs[i], void *));
}
for (i = 0; i < nb_rx; i++) {
// 此时 mbuf 数据已在缓存中,处理延迟极低
process_packet(bufs[i]);
}

2. 批量 DMA 提交

网卡硬件支持批量 DMA——一次处理多个描述符比逐个处理效率更高。批量提交减少了 MMIO 写操作(写 TDT/RDT 寄存器)的次数,而 MMIO 写操作是昂贵的(通常需要几百纳秒)。

3. 分摊固定开销

每次 rte_eth_rx_burst() 调用都有固定开销(检查描述符状态、更新指针等)。批量越大,每个包分摊的固定开销越低。

Burst Size每包固定开销占比缓存命中率典型场景
1高(~30%)极低延迟场景
8中(~10%)低延迟场景
32低(~3%)通用数据平面
64+极低(~1%)极高批量转发、吞吐优先

3.4 向量化收发包#

现代 PMD 驱动(如 Intel 的 ice、mlx5)支持向量化收发包——利用 CPU 的 SIMD 指令(SSE/AVX/AVX2/AVX-512)一次处理多个描述符:

// 向量化 RX 的核心思想(简化)
// 一次检查 4 个或 8 个描述符的 DD 位
__m256i desc_vals = _mm256_loadu_si256((__m256i *)desc_ptr);
__m256i dd_mask = _mm256_set1_epi32(DD_BIT);
__m256i cmp = _mm256_and_si256(desc_vals, dd_mask);
// 一次比较得到 8 个描述符的状态

向量化收发包可以将单核吞吐量提升 30%~50%。DPDK 在编译时根据 CPU 支持的指令集自动选择最优的向量化路径:

# 编译时查看向量化支持
meson configure build | grep vector
# 运行时查看当前使用的向量化路径
# 在 testpmd 中:
# show config rxtx
# Rx mode: vectorized (AVX2)
Warning

向量化路径通常不支持所有 offload 特性。如果你的应用需要 VLAN 剥离、校验和卸载等高级特性,可能需要回退到非向量化路径。在 rte_eth_dev_configure() 中请求的 offload 会影响 PMD 是否选择向量化路径。

四、VFIO 与 UIO:设备访问的两种路径#

PMD 要在用户态直接操作网卡硬件,必须解决一个根本问题:用户态程序如何访问网卡的寄存器和 DMA 内存? Linux 提供了两种框架:UIO 和 VFIO。

4.1 UIO(Userspace I/O)#

UIO 是最简单的用户态设备访问框架。它将网卡的 PCI BAR(Base Address Register)空间映射到用户态,并提供一个文件描述符用于中断通知:

# 加载 UIO 驱动
sudo modprobe uio_pci_generic
# 绑定网卡到 UIO
sudo dpdk-devbind.py --bind=uio_pci_generic 0000:01:00.0

UIO 的工作原理:

  1. 设备注册uio_pci_generic 驱动接管网卡,将其 PCI BAR 空间通过 /dev/uioX 设备文件暴露
  2. 内存映射:用户态程序通过 mmap()/dev/uioX 映射到进程地址空间,直接读写网卡寄存器
  3. 中断等待:通过 read()poll() /dev/uioX 等待中断事件

UIO 的优点是简单——内核代码极少,几乎不会出 bug。但它的缺点也很明显:

  • 无 IOMMU 保护:DMA 传输不经过 IOMMU 地址转换,恶意或有缺陷的用户态程序可以通过 DMA 访问系统任意物理内存
  • 安全性差:任何能访问 /dev/uioX 的进程都能操作网卡
  • 不支持 VFIO group:无法实现设备隔离

4.2 VFIO(Virtual Function I/O)#

VFIO 是现代 DPDK 部署的推荐框架。它基于 Linux IOMMU(Input/Output Memory Management Unit)提供安全的设备访问:

# 启用 IOMMU(在内核启动参数中添加)
# Intel: intel_iommu=on
# AMD: amd_iommu=on
# 加载 VFIO 驱动
sudo modprobe vfio-pci
# 绑定网卡到 VFIO
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.0
# 验证绑定
dpdk-devbind.py --status

VFIO 的核心安全机制:

机制说明
IOMMU 隔离DMA 传输经过 IOMMU 地址转换,设备只能访问已映射的内存
IOMMU Group同一 IOMMU Group 中的设备共享地址翻译表,实现设备级隔离
DMA 映射用户态程序必须显式注册 DMA 缓冲区,IOMMU 只允许访问已注册的区域
权限控制通过 /dev/vfio/X 设备文件的 Unix 权限控制访问

4.3 VFIO 的 IOMMU Group 机制#

IOMMU Group 是 VFIO 安全模型的基础。同一 Group 中的设备共享 IOMMU 地址翻译上下文,因此必须作为一个整体分配给同一个用户态进程:

# 查看设备的 IOMMU Group
ls -la /sys/bus/pci/devices/0000:01:00.0/iommu_group/devices/
# 查看 IOMMU Group 编号
readlink /sys/bus/pci/devices/0000:01:00.0/iommu_group
# 输出类似: ../../kernel/iommu_groups/1
# 如果 Group 中有多个设备,必须全部绑定到 VFIO 或全部不绑定
Warning

如果 IOMMU Group 中包含多个设备(例如网卡及其 SR-IOV VF),你必须将 Group 中的所有设备都绑定到 vfio-pci,否则 VFIO 无法获得该 Group 的独占访问权。这是初学者常遇到的绑定失败原因。

4.4 设备绑定完整流程#

以下是将网卡从内核驱动绑定到 VFIO 的完整操作步骤:

# 第一步:查看当前网卡状态
dpdk-devbind.py --status
# 输出示例:
# 0000:01:00.0 '82599ES 10-Gigabit' if=eth0 drv=ixgbe unused=vfio-pci
# 0000:01:00.1 '82599ES 10-Gigabit' if=eth1 drv=ixgbe unused=vfio-pci
# 第二步:卸载当前内核驱动
sudo rmmod ixgbe
# 第三步:加载 VFIO 驱动
sudo modprobe vfio-pci
# 第四步:绑定到 VFIO
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.0
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.1
# 第五步:验证绑定
dpdk-devbind.py --status
# 输出示例:
# 0000:01:00.0 '82599ES 10-Gigabit' drv=vfio-pci unused=ixgbe
# 0000:01:00.1 '82599ES 10-Gigabit' drv=vfio-pci unused=ixgbe
# 第六步:设置 VFIO 权限(非 root 用户需要)
sudo chmod 666 /dev/vfio/1
# 或者将用户加入 vfio 组
sudo usermod -aG vfio $USER
Note

DPDK 23.11+ 推荐使用 VFIO 而非 UIO。VFIO 不仅更安全,还支持更多高级特性:IOMMU DMA 映射、SR-IOV VF 独立分配、设备热插拔等。在生产环境中,VFIO 是唯一推荐的选择。

4.5 VFIO 与 DPDK 内存管理的关系#

第 4 章中讨论了 DPDK 的大页内存和 mempool。当使用 VFIO 时,DPDK 的内存管理与之紧密协作:

  1. DPDK 通过 rte_eal_init() 初始化大页内存
  2. EAL 调用 VFIO 的 vfio_dma_map API,将大页内存注册到 IOMMU
  3. IOMMU 为这些内存创建 IOVA(I/O Virtual Address)映射
  4. 网卡 DMA 使用 IOVA 地址,IOMMU 将其翻译为物理地址
// DPDK EAL 内部的 VFIO DMA 映射(简化)
// 将大页内存映射到 IOMMU
int vfio_dma_map(uint64_t vaddr, uint64_t iova, uint64_t size)
{
struct vfio_iommu_type1_dma_map dma_map = {
.argsz = sizeof(dma_map),
.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
.vaddr = vaddr, // 虚拟地址
.iova = iova, // I/O 虚拟地址
.size = size, // 映射大小
};
return ioctl(vfio_container_fd, VFIO_IOMMU_MAP_DMA, &dma_map);
}

这条链路确保了网卡 DMA 只能访问 DPDK 注册的内存区域,不会越界读写系统其他内存——这是 VFIO 相对于 UIO 的核心安全优势。

五、SR-IOV:虚拟功能与硬件隔离#

5.1 SR-IOV 概述#

SR-IOV(Single Root I/O Virtualization)是 PCI 标准定义的一种硬件虚拟化技术,它允许一块物理网卡(PF,Physical Function)虚拟出多个虚拟功能(VF,Virtual Function),每个 VF 拥有独立的 PCI 配置空间、DMA 引擎和中断,可以像独立网卡一样分配给虚拟机或容器使用。

SR-IOV 的核心价值在于硬件级隔离:VF 之间的数据路径完全由网卡硬件隔离,不涉及软件交换,性能接近物理网卡直通。

5.2 PF 与 VF 的关系#

flowchart TB subgraph 物理网卡["物理网卡 (PCIe 设备)"] PF["PF (Physical Function)<br/>完整管理功能<br/>配置 VF、流规则、统计"] VF0["VF 0<br/>独立收发包<br/>独立 DMA/中断"] VF1["VF 1<br/>独立收发包<br/>独立 DMA/中断"] VF2["VF 2<br/>独立收发包<br/>独立 DMA/中断"] VF3["VF 3<br/>独立收发包<br/>独立 DMA/中断"] end subgraph 宿主机["宿主机"] PF_DRV["PF 驱动<br/>ixgbe / ice / mlx5"] VF_DRV["VF 驱动<br/>ixgbevf / icevf / mlx5vf"] end subgraph 虚拟机["虚拟机"] VM0["VM 0<br/>VF 0 直通"] VM1["VM 1<br/>VF 1 直通"] VM2["VM 2<br/>VF 2 直通"] end PF --> PF_DRV VF0 --> VF_DRV VF1 --> VF_DRV VF0 -.->|"PCI Passthrough"| VM0 VF1 -.->|"PCI Passthrough"| VM1 VF2 -.->|"PCI Passthrough"| VM2 PF -.->|"管理/配置"| VF0 PF -.->|"管理/配置"| VF1 PF -.->|"管理/配置"| VF2 style 物理网卡 fill:#fff3e0,stroke:#e65100 style 宿主机 fill:#e3f2fd,stroke:#1565c0 style 虚拟机 fill:#e8f5e9,stroke:#2e7d32

PF 与 VF 的关键区别:

特性PFVF
数量每个物理端口 1 个最多可达 256 个(取决于网卡型号)
管理能力完整:配置 VF、流规则、MAC/VLAN有限:只能管理自己的收发包
收发包可以收发所有流量只能收发分配给自己的流量
MAC 地址固定 MAC + 可配置可由 PF 分配或 VF 自行设置
中断完整中断支持独立中断向量
DPDK 驱动PMD PF 驱动PMD VF 驱动

5.3 SR-IOV 配置步骤#

# 第一步:启用 SR-IOV(创建 VF)
# 在 PF 上创建 4 个 VF
echo 4 | sudo tee /sys/class/net/eth0/device/sriov_numvfs
# 验证 VF 创建
lspci | grep -i virtual
# 输出示例:
# 01:10.0 Ethernet controller: Intel 82599 Virtual Function
# 01:10.1 Ethernet controller: Intel 82599 Virtual Function
# 01:10.2 Ethernet controller: Intel 82599 Virtual Function
# 01:10.3 Ethernet controller: Intel 82599 Virtual Function
# 第二步:设置 VF 的 MAC 地址(可选,由 PF 管理)
sudo ip link set eth0 vf 0 mac 00:11:22:33:44:55
sudo ip link set eth0 vf 1 mac 00:11:22:33:44:66
# 第三步:设置 VF 的 VLAN(可选)
sudo ip link set eth0 vf 0 vlan 100
sudo ip link set eth0 vf 1 vlan 200
# 第四步:查看 VF 状态
ip link show eth0
# 输出包含 VF 信息:
# vf 0 MAC 00:11:22:33:44:55, vlan 100
# vf 1 MAC 00:11:22:33:44:66, vlan 200
# vf 2 MAC 00:00:00:00:00:00
# vf 3 MAC 00:00:00:00:00:00
# 第五步:将 VF 分配给虚拟机(libvirt 示例)
# 在 VM XML 中添加:
# <interface type='hostdev'>
# <source>
# <address type='pci' domain='0x0000' bus='0x01' slot='0x10' function='0x0'/>
# </source>
# </interface>

5.4 DPDK VF 驱动使用#

VF 可以像物理网卡一样被 DPDK PMD 驱动接管:

# 将 VF 绑定到 VFIO
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:10.0
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:10.1
# 使用 testpmd 测试 VF 收发包
dpdk-testpmd -l 0-3 -n 4 -- -i
# 在 testpmd 中查看 VF 端口
show port info all

在 DPDK 应用中,VF 的初始化与 PF 完全一致——使用相同的 rte_eth_dev_configure / rte_eth_rx_queue_setup / rte_eth_tx_queue_setup / rte_eth_dev_start 序列。rte_ethdev 抽象层屏蔽了 PF 和 VF 的差异。

Note

VF 的 MAC 地址管理有两种模式:PF 管理(由 PF 驱动设置 VF 的 MAC,VF 无法修改)和 VF 自主(VF 可以自行设置 MAC 地址)。安全场景下应使用 PF 管理模式,防止 VF 伪造 MAC 地址进行网络攻击。通过 ip link set <pf> vf <id> mac <addr> 设置的 MAC 地址是 PF 强制指定的,VF 无法覆盖。

5.5 SR-IOV 与 DPDK 的协作模式#

在实际部署中,SR-IOV 与 DPDK 有两种典型协作模式:

模式一:PF 内核驱动 + VF DPDK 驱动

PF 继续由内核驱动管理(处理管理流量),VF 由 DPDK PMD 接管(处理数据面流量)。这是最常见的部署方式:

# PF 保持内核驱动(ixgbe)
# VF 绑定到 VFIO 供 DPDK 使用
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:10.0

模式二:PF DPDK 驱动 + VF DPDK 驱动

PF 和 VF 都由 DPDK PMD 接管。适用于完全用户态的数据面场景,但 PF 的管理功能需要通过 DPDK 的 rte_eth_dev API 实现。

Warning

当 PF 由内核驱动管理时,不要将 PF 绑定到 VFIO——这会导致所有 VF 失效。如果需要 PF 也使用 DPDK,必须确保 VF 已创建后再绑定 PF。

六、Bond 驱动:链路聚合#

6.1 为什么需要链路聚合#

在生产环境中,单块网卡的带宽和可靠性往往不够:

  • 带宽不足:单端口 25GbE 无法满足业务需求,需要聚合多条链路
  • 冗余需求:单点故障不可接受,需要主备切换
  • 负载均衡:多链路分担流量,提高整体吞吐

DPDK Bond 驱动(rte_eth_bond)提供了用户态的链路聚合功能,与 Linux 内核的 bonding 驱动功能对应,但性能远超内核方案。

6.2 Bond 模式详解#

DPDK Bond 支持以下模式:

模式名称机制特点
Mode 0Round-robin轮询分发到所有从端口简单,但不保证包顺序
Mode 1Active-backup只有一个活跃端口,其余热备高可用,带宽不增加
Mode 2Balance XOR基于 MAC 地址 XOR 哈希选择端口同一流走同一条链路
Mode 3Broadcast所有包从所有端口发出可靠性最高,带宽浪费
Mode 4LACP (802.3ad)基于 LACP 协议协商聚合工业标准,需要交换机支持

Mode 0 — Round-robin:最简单的负载均衡模式,数据包轮询分发到所有从端口。优点是带宽可以叠加,缺点是同一流的包可能乱序到达,对 TCP 性能影响较大。

Mode 1 — Active-backup:只有一个活跃端口转发流量,其余端口处于热备状态。当活跃端口链路故障时,自动切换到备用端口。优点是简单可靠,缺点是带宽只有单端口水平。

Mode 4 — LACP:工业标准的链路聚合协议。通过 LACPDU 报文与对端交换机协商聚合组,支持故障检测和流量分担。这是生产环境中最常用的模式:

// 创建 LACP Bond 设备
#include <rte_eth_bond.h>
// 创建 bond 设备
uint16_t bond_port_id;
bond_port_id = rte_eth_bond_create("bond0", BONDING_MODE_8023AD, socket_id);
// 添加从端口
rte_eth_bond_slave_add(bond_port_id, port0);
rte_eth_bond_slave_add(bond_port_id, port1);
// 配置 LACP 参数
struct rte_eth_bond_8023ad_conf lacp_conf;
rte_eth_bond_8023ad_conf_get(bond_port_id, &lacp_conf);
lacp_conf.fast_periodic_ms = 500; // 快速周期
lacp_conf.slow_periodic_ms = 30000; // 慢速周期
lacp_conf.aggregate_wait_timeout_ms = 500;
rte_eth_bond_8023ad_conf_set(bond_port_id, &lacp_conf);

6.3 Bond 设备的初始化#

// 完整的 Bond 设备初始化流程
int bond_init(struct rte_mempool *mbuf_pool)
{
uint16_t bond_port_id;
int ret;
// 1. 创建 bond 设备(Mode 4: LACP)
bond_port_id = rte_eth_bond_create("net_bonding0",
BONDING_MODE_8023AD, rte_socket_id());
if (bond_port_id < 0)
rte_exit(EXIT_FAILURE, "Failed to create bond device\n");
// 2. 添加从端口
ret = rte_eth_bond_slave_add(bond_port_id, 0); // port 0
if (ret < 0)
rte_exit(EXIT_FAILURE, "Failed to add slave 0\n");
ret = rte_eth_bond_slave_add(bond_port_id, 1); // port 1
if (ret < 0)
rte_exit(EXIT_FAILURE, "Failed to add slave 1\n");
// 3. 配置 bond 设备(与普通端口相同)
ret = port_init(bond_port_id, mbuf_pool);
if (ret < 0)
rte_exit(EXIT_FAILURE, "Failed to configure bond port\n");
// 4. 启动 bond 设备
ret = rte_eth_dev_start(bond_port_id);
if (ret < 0)
rte_exit(EXIT_FAILURE, "Failed to start bond port\n");
return 0;
}
Note

Bond 设备在 rte_ethdev 层面与普通端口完全一致——应用代码不需要区分 bond 端口和物理端口。所有 rte_eth_rx_burst / rte_eth_tx_burst 调用都透明地经过 Bond 驱动的分发逻辑。

七、rte_flow:流导向与硬件卸载#

7.1 为什么需要流导向#

第 3 章中提到,现代网卡支持多队列(Multi-Queue),通过 RSS(Receive Side Scaling)将不同流的包分发到不同队列,由不同 lcore 并行处理。但 RSS 只能做简单的哈希分流,无法实现精确的流控制:

  • “将目的端口为 80 的包送到队列 0,其余送到队列 1”——RSS 做不到
  • “丢弃所有来自某个 IP 的包”——RSS 做不到
  • “将某个 TCP 流的包标记后转发到虚拟交换机”——RSS 做不到

这些需求需要流导向(Flow Steering)——根据包的匹配规则执行特定动作。rte_flow 就是 DPDK 提供的通用流导向 API,它将匹配规则和动作抽象为硬件无关的接口,由 PMD 驱动翻译为网卡特定的硬件规则。

7.2 rte_flow 规则模型#

rte_flow 规则由两部分组成:Pattern(匹配模式)Actions(动作列表)

rte_flow 规则 = Pattern + Actions
Pattern = 一组匹配项(item),从外层到内层
Actions = 一组动作(action),按顺序执行

Pattern 示例:匹配以太网 → IPv4 → TCP,目的端口 80

// 定义匹配模式
struct rte_flow_attr attr = {
.ingress = 1, // 入方向规则
};
struct rte_flow_item pattern[4]; // 最后一项必须是 END
// Item 0: 以太网层
struct rte_flow_item_eth eth_spec = {
.type = RTE_BE16(RTE_ETHER_TYPE_IPV4),
};
struct rte_flow_item_eth eth_mask = {
.type = 0xFFFF, // 匹配 EtherType
};
pattern[0].type = RTE_FLOW_ITEM_TYPE_ETH;
pattern[0].spec = &eth_spec;
pattern[0].mask = &eth_mask;
// Item 1: IPv4 层
struct rte_flow_item_ipv4 ipv4_spec = {0};
struct rte_flow_item_ipv4 ipv4_mask = {0};
pattern[1].type = RTE_FLOW_ITEM_TYPE_IPV4;
pattern[1].spec = &ipv4_spec;
pattern[1].mask = &ipv4_mask;
// Item 2: TCP 层,匹配目的端口 80
struct rte_flow_item_tcp tcp_spec = {
.hdr.dst_port = RTE_BE16(80),
};
struct rte_flow_item_tcp tcp_mask = {
.hdr.dst_port = 0xFFFF, // 完全匹配目的端口
};
pattern[2].type = RTE_FLOW_ITEM_TYPE_TCP;
pattern[2].spec = &tcp_spec;
pattern[2].mask = &tcp_mask;
// Item 3: 结束标记
pattern[3].type = RTE_FLOW_ITEM_TYPE_END;

Actions 示例:将匹配的包送到队列 0

// 定义动作列表
struct rte_flow_action actions[2];
// Action 0: 送到队列 0
struct rte_flow_action_queue queue_action = {
.index = 0,
};
actions[0].type = RTE_FLOW_ACTION_TYPE_QUEUE;
actions[0].conf = &queue_action;
// Action 1: 结束标记
actions[1].type = RTE_FLOW_ACTION_TYPE_END;

创建流规则

struct rte_flow_error error;
// 验证规则是否被硬件支持
int ret = rte_flow_validate(port_id, &attr, pattern, actions, &error);
if (ret < 0) {
printf("Flow rule validation failed: %s\n", error.message);
return -1;
}
// 创建流规则
struct rte_flow *flow = rte_flow_create(port_id, &attr, pattern, actions, &error);
if (flow == NULL) {
printf("Flow rule creation failed: %s\n", error.message);
return -1;
}
// 销毁流规则(不再需要时)
rte_flow_destroy(port_id, flow, &error);
// 销毁端口上的所有流规则
rte_flow_flush(port_id, &error);

7.3 rte_flow 支持的动作类型#

动作说明典型用途
QUEUE将包送到指定 RX 队列流量分流
DROP丢弃包访问控制、DDoS 防护
MARK给包打标记(标记可通过 mbuf 的 hash 字段读取)流量分类
RSS使用 RSS 哈希分发到多个队列多核负载均衡
VF将包送到指定 VFSR-IOV 虚拟交换
PORT_ID将包转到另一个端口硬件转发
COUNT统计匹配的包数量流量监控
SET_MAC_SRC/DST修改源/目的 MACNAT、路由
SET_IPV4_SRC/DST修改源/目的 IPNAT
SET_TP_SRC/DST修改源/目的端口号NAT、端口映射

7.4 RSS:接收侧缩放#

RSS(Receive Side Scaling)是 rte_flow 的一个重要动作,它利用网卡的哈希引擎将包分发到多个 RX 队列:

// 配置 RSS:基于 IPv4 + TCP 四元组哈希
struct rte_eth_rss_conf rss_conf = {
.rss_key = NULL, // 使用默认 RSS key
.rss_key_len = 0,
.rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP, // 哈希字段
};
// 在 port_conf 中配置 RSS
struct rte_eth_conf port_conf = {
.rxmode = {
.mq_mode = RTE_ETH_MQ_RX_RSS,
.offloads = 0,
},
.rx_adv_conf = {
.rss_conf = rss_conf,
},
};

RSS 的工作原理:

  1. 网卡对每个收到的包计算一个哈希值(通常基于五元组:源IP、目的IP、源端口、目的端口、协议)
  2. 哈希值对队列数量取模,决定包送到哪个队列
  3. 同一条流的包始终哈希到同一个队列,保证包顺序
Note

RSS 的哈希字段选择直接影响多核负载均衡的效果。如果只基于 IP 哈希(RTE_ETH_RSS_IP),同一 IP 对的所有包会到同一个队列;如果加上端口哈希(RTE_ETH_RSS_TCP),不同连接的包可以分散到不同队列。选择哪种取决于你的流量特征。

7.5 Flow Director:精确流导向#

Flow Director 是 Intel 网卡提供的精确流匹配引擎,它比 RSS 更精确——RSS 只能做哈希分流,Flow Director 可以做精确匹配并执行特定动作:

// 使用 rte_flow 实现 Flow Director 功能
// 匹配:源 IP = 192.168.1.100,目的端口 = 443
// 动作:送到队列 2,并打标记 0x1234
struct rte_flow_item_ipv4 ipv4_spec = {
.hdr.src_addr = RTE_BE32(0xC0A80164), // 192.168.1.100
};
struct rte_flow_item_ipv4 ipv4_mask = {
.hdr.src_addr = 0xFFFFFFFF, // 完全匹配源 IP
};
struct rte_flow_item_tcp tcp_spec = {
.hdr.dst_port = RTE_BE16(443),
};
struct rte_flow_item_tcp tcp_mask = {
.hdr.dst_port = 0xFFFF,
};
struct rte_flow_action_queue queue_action = { .index = 2 };
struct rte_flow_action_mark mark_action = { .id = 0x1234 };
struct rte_flow_action actions[] = {
{ .type = RTE_FLOW_ACTION_TYPE_MARK, .conf = &mark_action },
{ .type = RTE_FLOW_ACTION_TYPE_QUEUE, .conf = &queue_action },
{ .type = RTE_FLOW_ACTION_TYPE_END },
};

7.6 硬件卸载的层次#

rte_flow 实现了不同层次的硬件卸载,从简单到复杂:

flowchart TB subgraph 卸载层次["硬件卸载层次"] L1["Level 1: RSS 哈希分流<br/>最基础,几乎所有网卡支持"] L2["Level 2: Flow Director 精确匹配<br/>主流网卡支持"] L3["Level 3: rte_flow 通用规则<br/>支持复杂匹配+动作组合"] L4["Level 4: 完全硬件转发<br/>SmartNIC/DPU 支持"] end L1 --> L2 --> L3 --> L4 subgraph 典型应用["典型应用场景"] A1["多核负载均衡"] A2["流量分类与过滤"] A3["OVS 流表卸载"] A4["可编程数据平面"] end L1 -.-> A1 L2 -.-> A2 L3 -.-> A3 L4 -.-> A4 style 卸载层次 fill:#e3f2fd,stroke:#1565c0 style 典型应用 fill:#e8f5e9,stroke:#2e7d32
Warning

rte_flow 规则的数量受网卡硬件资源限制。Intel XL710 的 Flow Director 最多支持约 8K 条规则,Mellanox ConnectX-5 最多支持约 128K 条规则。如果你的流表规模超过硬件限制,需要考虑软件回退方案或使用 SmartNIC/DPU(将在第 11 章中讨论)。

八、动手实践#

实践 1:绑定网卡到 VFIO 并验证#

# 1. 确认网卡 PCI 地址
lspci | grep -i ethernet
# 示例输出:
# 01:00.0 Ethernet controller: Intel 82599ES 10-Gigabit
# 2. 查看当前驱动
dpdk-devbind.py --status
# 3. 加载 VFIO 驱动
sudo modprobe vfio-pci
# 4. 绑定网卡到 VFIO
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.0
# 5. 验证绑定成功
dpdk-devbind.py --status | grep -A2 "0000:01:00.0"
# 期望输出:drv=vfio-pci
# 6. 配置大页
echo 1024 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 7. 验证大页可用
cat /proc/meminfo | grep HugePages
# HugePages_Total: 1024
# HugePages_Free: 1024

实践 2:运行 testpmd 并观察 RX/TX 统计#

testpmd 是 DPDK 自带的包转发测试工具,是验证 PMD 功能的首选工具:

# 启动 testpmd
# -l 0-3: 使用 lcore 0~3
# -n 4: 4 个内存通道
# -- -i: 交互模式
dpdk-testpmd -l 0-3 -n 4 -- -i
# 在 testpmd 交互界面中:
# 查看端口信息
show port info all
# 启动转发
start
# 查看收发包统计(每秒刷新)
show port stats all
# 典型输出:
# RX-packets: 12345678 RX-missed: 0 RX-bytes: 876543210
# RX-errors: 0 RX-nombuf: 0
# TX-packets: 12345678 TX-errors: 0 TX-bytes: 876543210
# 查看 RX/TX 队列统计
show port xstats all
# 停止转发
stop
# 退出
quit
Note

RX-missed 表示网卡因 RX 描述符环满而丢弃的包,RX-nombuf 表示因 mempool 空间不足而丢弃的包。如果这两个值不为零,说明需要增大描述符环大小或 mempool 容量——这正是第 4 章中讨论的内存规划问题。

实践 3:配置 SR-IOV 虚拟功能#

# 1. 确认网卡支持 SR-IOV
lspci -vvv -s 0000:01:00.0 | grep -i "SR-IOV"
# 期望输出:Capabilities: [xxx] SR-IOV
# 2. 查看最大 VF 数量
cat /sys/class/net/eth0/device/sriov_totalvfs
# 输出:64(取决于网卡型号)
# 3. 创建 4 个 VF
echo 4 | sudo tee /sys/class/net/eth0/device/sriov_numvfs
# 4. 验证 VF 创建
lspci | grep -i "virtual function"
# 5. 查看 VF 状态
ip link show eth0 | grep vf
# 6. 设置 VF MAC 地址
sudo ip link set eth0 vf 0 mac 52:54:00:00:01:01
sudo ip link set eth0 vf 1 mac 52:54:00:00:01:02
# 7. 设置 VF VLAN
sudo ip link set eth0 vf 0 vlan 100
# 8. 将 VF 绑定到 VFIO 供 DPDK 使用
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:10.0
# 9. 用 testpmd 测试 VF
dpdk-testpmd -l 0-1 -n 4 -- -i
show port info all

实践 4:使用 testpmd 创建 rte_flow 规则#

# 启动 testpmd
dpdk-testpmd -l 0-3 -n 4 -- -i
# 创建流规则:将目的端口为 80 的 TCP 包送到队列 0
flow create 0 ingress pattern eth / ipv4 / tcp dst is 80 / end actions queue index 0 / end
# 创建流规则:丢弃来自 192.168.1.100 的所有包
flow create 0 ingress pattern eth / ipv4 src is 192.168.1.100 / end actions drop / end
# 创建流规则:将 TCP 443 流量标记为 0x1234 并送到队列 1
flow create 0 ingress pattern eth / ipv4 / tcp dst is 443 / end actions mark id 0x1234 / queue index 1 / end
# 查看所有流规则
flow list 0
# 查看流规则统计
flow query 0 0 count
# 输出:hits: 12345
# 销毁特定流规则
flow destroy 0 rule 0
# 销毁所有流规则
flow flush 0

小结#

本章深入剖析了 DPDK 轮询模式驱动(PMD)的完整技术栈:

  1. 轮询 vs 中断:PMD 用轮询替代中断,牺牲 CPU 空闲时间换取零中断开销和极低延迟。混合模式在低流量时切换到中断以节省电力,是 DPDK 22.11+ 的重要演进。

  2. rte_ethdev 抽象层:统一的以太网设备 API,从 rte_eth_dev_configurerte_eth_rx_burst / rte_eth_tx_burst,屏蔽了物理网卡、VF、Bond 等设备差异。

  3. RX/TX 队列与批量操作:描述符环是 PMD 与网卡硬件协作的核心数据结构,批量收发(burst)通过缓存预取、批量 DMA、分摊固定开销三重机制大幅提升吞吐量,向量化路径进一步压榨硬件性能。

  4. VFIO 与 UIO:VFIO 基于 IOMMU 提供安全的设备访问,是生产环境的唯一推荐选择;UIO 简单但不安全,仅适合开发测试。设备绑定是 DPDK 应用的第一步操作。

  5. SR-IOV:硬件级虚拟化技术,一块物理网卡虚拟出多个 VF,每个 VF 拥有独立的收发能力和 DMA 引擎,是云环境高性能网络的基础设施。

  6. Bond 驱动:用户态链路聚合,支持 Round-robin、Active-backup、LACP 等模式,在 rte_ethdev 层面与普通端口完全一致。

  7. rte_flow:通用流导向 API,将匹配规则和动作抽象为硬件无关接口,支持 RSS 哈希分流、Flow Director 精确匹配、复杂规则卸载,是 OVS 硬件卸载和 SmartNIC 编程的基础。

PMD 是 DPDK 数据面的引擎——理解了 PMD,你就理解了数据包如何在用户态流动。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

部分信息可能已经过时

相关文章 智能推荐
1
OVS-DPDK 与虚拟交换
高性能网络 深入 OVS-DPDK 与虚拟交换——OVS-DPDK 架构与内核 OVS 对比、dpdkvhostuser 端口类型、vhost-user 协议与共享 virtio 环、virtio 前后端、VM-to-VM 交换路径、流分类(EMC/DPCLS/megaflows)、性能调优——掌握云环境虚拟网络加速的完整技术栈。
2
DPDK 多核与并发模型
高性能网络 深入 DPDK 多核与并发模型——lcore 模型与 CPU 亲和性、Run-to-Completion 模型、Pipeline 模型与 rte_ring 跨核通信、原子操作与内存屏障、RCU 机制(rte_rcu_qsbr)、Eventdev 事件驱动框架——掌握多核数据平面编程的完整技术栈。
3
DPDK 内存管理
高性能网络 深入 DPDK 内存管理——大页(Huge Pages)配置与 TLB 加速、mempool 分级缓存架构、rte_mbuf 结构与分段链、rte_malloc 分配器、NUMA 感知与跨节点惩罚、memzone 与 IOVA 模式——理解 DPDK 内存管理是掌握高性能数据平面的基石。
4
SmartNIC 与 DPU
高性能网络 深入 SmartNIC 与 DPU——硬件卸载概念与收益、SmartNIC 架构(固定功能 vs 可编程)、DPU 产品矩阵(NVIDIA BlueField/AMD Pensando/Intel IPU)、OVS 硬件卸载(tc/rte_flow/ASAP²)、P4 编程入门——掌握硬件加速网络的完整技术栈。
5
DPDK 数据平面核心机制
高性能网络 深入 DPDK 数据平面核心机制——rte_ring 无锁环形缓冲区(SPSC/MPMC/RTS/HTS)、rte_mbuf 分段链与零拷贝、包解析辅助库(rte_net/rte_ether/rte_ip/rte_tcp)、CRC/Hash 硬件卸载、TSO/LRO、Scatter-Gather I/O——掌握数据包在 DPDK 应用中的高效处理全链路。