【DPDK例程学习】(2) skeleton
DPDK Skeleton (basicfwd) 例程详解教程
学习目标:通过 DPDK 最简转发骨架,理解 DPDK 网络应用的三大核心机制 —— 网卡初始化流程、mbuf 内存池管理、RX/TX Burst 收发包模型。
目录
1. 源码逐行解读
整个例程仅一个 C 文件 basicfwd.c(221 行),是 DPDK 官方提供的 最小化二层包转发骨架。代码被官方文档多处引用(8< / >8 标记),是学习 DPDK 网络编程的最佳起点。
1.1 头文件与宏定义(第 1~18 行)
#include <stdint.h>
#include <inttypes.h>
#include <rte_eal.h> // ① EAL 初始化/清理
#include <rte_ethdev.h> // ② 以太网设备 API(核心)
#include <rte_cycles.h> // ③ 精确时间戳/延时
#include <rte_lcore.h> // ④ lcore 管理
#include <rte_mbuf.h> // ⑤ mbuf(数据包缓冲区)
#define RX_RING_SIZE 1024 // ⑥ RX 描述符环大小
#define TX_RING_SIZE 1024 // ⑦ TX 描述符环大小
#define NUM_MBUFS 8191 // ⑧ mbuf 池容量
#define MBUF_CACHE_SIZE 250 // ⑨ 每核 mbuf 缓存大小
#define BURST_SIZE 32 // ⑩ 每次收发包的批量大小
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | <rte_eal.h> |
EAL(环境抽象层),提供 rte_eal_init / rte_eal_cleanup,所有 DPDK 程序必备。 |
| ② | <rte_ethdev.h> |
最重要的新头文件。提供网卡配置、端口启停、收发 burst 等全部以太网设备操作 API。 |
| ③ | <rte_cycles.h> |
基于 TSC(Time Stamp Counter)的精确时间 API,本示例没有直接使用但属于 DPDK 标配头文件。 |
| ④ | <rte_lcore.h> |
lcore 管理,提供 rte_lcore_id()、rte_lcore_count() 等。 |
| ⑤ | <rte_mbuf.h> |
核心:mbuf(message buffer)是 DPDK 中表示数据包的数据结构,替代了 Linux 内核的 sk_buff。 |
⑥ ⑦ |
RX/TX_RING_SIZE = 1024 |
硬件描述符环大小。网卡通过环形队列(Descriptor Ring)与软件交互:RX Ring 存放"空闲 buffer 地址"供网卡写入收到的包,TX Ring 存放"待发送包地址"供网卡发送。1024 是 DPDK 推荐的默认值。 |
| ⑧ | NUM_MBUFS = 8191 |
mbuf 内存池中的总 buffer 数。8191 = 8192 - 1,是 2 的幂次减 1(优化内存对齐)。这个数量要能覆盖:RX Ring 中等待写入的 + 应用正在处理的 + TX Ring 中等待发送的 + 每个 lcore 的缓存。 |
| ⑨ | MBUF_CACHE_SIZE = 250 |
每个 lcore 的本地 mbuf 缓存大小。减少多核之间对同一 mempool 的锁竞争。 |
| ⑩ | BURST_SIZE = 32 |
每次调用 rte_eth_rx_burst 最多收 32 个包。批量处理是 DPDK 高性能的关键:一次函数调用处理一批包,摊薄了函数调用开销和内存访问延迟。 |
1.2 端口初始化 port_init(第 27~107 行)
/* Main functional part of port initialization. 8< */
static inline int
port_init(uint16_t port, struct rte_mempool *mbuf_pool)
{
struct rte_eth_conf port_conf; // ① 端口配置结构体
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;
memset(&port_conf, 0, sizeof(struct rte_eth_conf));
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;
}
if (dev_info.tx_offload_capa & RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE) // ⑤
port_conf.txmode.offloads |= RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE;
/* Configure the Ethernet device. */
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;
/* Allocate and set up 1 RX queue per Ethernet port. */
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;
}
txconf = dev_info.default_txconf;
txconf.offloads = port_conf.txmode.offloads;
/* Allocate and set up 1 TX queue per Ethernet port. */
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;
}
/* Starting Ethernet port. 8< */
retval = rte_eth_dev_start(port); // ⑩ 启动端口
/* >8 End of starting of ethernet port. */
if (retval < 0)
return retval;
/* Display the port MAC address. */
struct rte_ether_addr addr;
retval = rte_eth_macaddr_get(port, &addr); // ⑪ 获取 MAC
if (retval != 0)
return retval;
printf("Port %u MAC: %02" PRIx8 " %02" PRIx8 " %02" PRIx8
" %02" PRIx8 " %02" PRIx8 " %02" PRIx8 "\n",
port, RTE_ETHER_ADDR_BYTES(&addr));
/* Enable RX in promiscuous mode for the Ethernet device. */
retval = rte_eth_promiscuous_enable(port); // ⑫ 混杂模式
if (retval != 0)
return retval;
return 0;
}
/* >8 End of main functional part of port initialization. */
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | struct rte_eth_conf port_conf |
端口全局配置:RX/TX 模式、offload 特性、RSS、DCB、VLAN 等。这里 memset 为 0 表示全部使用默认值。 |
| ② | rx_rings = 1, tx_rings = 1 |
这个例程每个端口只配 1 个 RX 队列和 1 个 TX 队列。意味着只有 1 个 lcore 能操作这个端口(如果你有多个 lcore 也要排队等同一个队列)。这也是为什么第 209 行会警告多 lcore 无意义。 |
| ③ | rte_eth_dev_is_valid_port() |
检查 port ID 是否对应一个已探测到的网卡。 |
| ④ | rte_eth_dev_info_get() |
关键调用:从网卡驱动获取硬件能力(offload 特性、速度、队列限制等)。之后的所有配置都要参考这个 info。 |
| ⑤ | RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE |
硬件 offload:网卡硬件在发送完成后自动释放 mbuf 回 mempool,省去软件干预。如果硬件支持就开启。 |
| ⑥ | rte_eth_dev_configure() |
正式配置网卡:指定队列数量 + 配置结构体。这个调用会触发网卡驱动的内部初始化。注意:必须在队列 setup 之前调用。 |
| ⑦ | rte_eth_dev_adjust_nb_rx_tx_desc() |
将请求的描述符数量调整为硬件支持的范围(某些网卡只支持特定数量,如 128/256/512…)。nb_rxd 和 nb_txd 会被修改为实际可用值。 |
| ⑧ | rte_eth_rx_queue_setup() |
为指定队列分配硬件 RX 描述符环,并绑定到 mbuf_pool。网卡收到包后,会从这个 pool 中取 mbuf 写入数据。 |
| ⑨ | rte_eth_tx_queue_setup() |
为指定队列分配硬件 TX 描述符环。TX 不绑定 mempool(要发送的 mbuf 由应用提供)。 |
| ⑩ | rte_eth_dev_start() |
启动网卡:开启 RX/TX 引擎,开始接收和发送数据包。在此之前网卡硬件不会处理任何包。 |
| ⑪ | rte_eth_macaddr_get() |
读取网卡的 MAC 地址并打印,用于调试确认网卡身份。 |
| ⑫ | rte_eth_promiscuous_enable() |
开启混杂模式:网卡接收所有经过的包(而不只是发给自己 MAC 的包)。转发设备必须开启,否则非本机 MAC 的包会被硬件丢弃。 |
1.3 转发主循环 lcore_main(第 114~165 行)
/* Basic forwarding application lcore. 8< */
static __rte_noreturn void
lcore_main(void)
{
uint16_t port;
/*
* Check that the port is on the same NUMA node as the polling thread
* for best performance.
*/
RTE_ETH_FOREACH_DEV(port) // ①
if (rte_eth_dev_socket_id(port) >= 0 &&
rte_eth_dev_socket_id(port) !=
(int)rte_socket_id()) // ② NUMA 检查
printf("WARNING, port %u is on remote NUMA node to "
"polling thread.\n\tPerformance will "
"not be optimal.\n", port);
printf("\nCore %u forwarding packets. [Ctrl+C to quit]\n",
rte_lcore_id());
/* Main work of application loop. 8< */
for (;;) { // ③ 无限循环
RTE_ETH_FOREACH_DEV(port) { // ④ 遍历所有端口
/* Get burst of RX packets, from first port of pair. */
struct rte_mbuf *bufs[BURST_SIZE]; // ⑤
const uint16_t nb_rx = rte_eth_rx_burst(port, 0, // ⑥ RX Burst
bufs, BURST_SIZE);
if (unlikely(nb_rx == 0)) // ⑦ 无包跳过
continue;
/* Send burst of TX packets, to second port of pair. */
const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0, // ⑧ TX Burst
bufs, nb_rx);
/* Free any unsent packets. */
if (unlikely(nb_tx < nb_rx)) { // ⑨ 释放未发送的包
uint16_t buf;
for (buf = nb_tx; buf < nb_rx; buf++)
rte_pktmbuf_free(bufs[buf]);
}
}
}
/* >8 End of loop. */
}
/* >8 End Basic forwarding application lcore. */
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | RTE_ETH_FOREACH_DEV(port) |
遍历所有已探测的网卡端口。与 RTE_LCORE_FOREACH_WORKER 类似的遍历宏,这里用于 NUMA 亲和性检查。 |
| ② | rte_eth_dev_socket_id() vs rte_socket_id() |
NUMA 感知:比较网卡所在的 NUMA node 和当前 lcore 所在的 NUMA node。跨 NUMA node 访问内存延迟显著增加,只给警告但不阻止运行。 |
| ③ | for (;;) |
无锁轮询模式(Run-to-Completion):DPDK 的典型工作模式,不是事件驱动,而是持续轮询。CPU 100% 运行,没有中断开销,因此能达到极高的吞吐和低延迟。 |
| ④ | RTE_ETH_FOREACH_DEV(port) |
每轮循环遍历所有端口收包。在单核单队列场景下是轮询所有端口——如果端口很多可能会有处理延迟。 |
| ⑤ | struct rte_mbuf *bufs[32] |
栈上的指针数组,存放收到的 mbuf 指针。注意:这些指针指向的是 mempool 中的 mbuf,不是数据本身。 |
| ⑥ | rte_eth_rx_burst(port, 0, bufs, 32) |
核心 API:从端口 port 的队列 0 批量收包,最多 32 个。返回值是实际收到的包数量。底层操作:从 RX 描述符环读取 DMA 已完成的条目,将 mbuf 指针传给应用,然后在环中补充新的空 mbuf。 |
| ⑦ | unlikely(nb_rx == 0) |
unlikely() 是 Linux 内核风格的分支预测提示宏,告诉 CPU “这个分支不太可能发生”。空收是常见状态(没有包来),这样标记可以优化 CPU 流水线。 |
| ⑧ | rte_eth_tx_burst(port ^ 1, 0, bufs, nb_rx) |
关键转发逻辑:发往配对端口。port ^ 1 是 XOR 运算 → 0↔1、2↔3、4↔5。TX Burst 是"尽力而为"的——如果 TX 描述符环满了,返回值会 < 请求数。这就是需要第 ⑨ 步的原因。 |
| ⑨ | rte_pktmbuf_free() |
释放未成功发送的 mbuf 回 mempool。不释放会导致内存泄漏:mbuf 被"吞掉"不回池,最终池空,网卡无 buffer 可用而丢包。 |
1.4 主函数 main(第 171~220 行)
int
main(int argc, char *argv[])
{
struct rte_mempool *mbuf_pool;
unsigned nb_ports;
uint16_t portid;
/* Initializion the Environment Abstraction Layer (EAL). 8< */
int ret = rte_eal_init(argc, argv); // ① EAL 初始化
if (ret < 0)
rte_exit(EXIT_FAILURE, "Error with EAL initialization\n");
/* >8 End of initialization ... */
argc -= ret; // ② 剥离 EAL 参数
argv += ret;
/* Check that there is an even number of ports ... */
nb_ports = rte_eth_dev_count_avail(); // ③ 统计可用端口
if (nb_ports < 2 || (nb_ports & 1))
rte_exit(EXIT_FAILURE, "Error: number of ports must be even\n");
/* Allocates mempool to hold the mbufs. 8< */
mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", // ④ 创建 mbuf 池
NUM_MBUFS * nb_ports,
MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
rte_socket_id());
/* >8 End of allocating mempool to hold mbuf. */
if (mbuf_pool == NULL)
rte_exit(EXIT_FAILURE, "Cannot create mbuf pool\n");
/* Initializing all ports. 8< */
RTE_ETH_FOREACH_DEV(portid) // ⑤ 初始化所有端口
if (port_init(portid, mbuf_pool) != 0)
rte_exit(EXIT_FAILURE, "Cannot init port %"PRIu16 "\n",
portid);
/* >8 End of initializing all ports. */
if (rte_lcore_count() > 1) // ⑥ 多核警告
printf("\nWARNING: Too many lcores enabled. Only 1 used.\n");
/* Call lcore_main on the main core only. ... 8< */
lcore_main(); // ⑦ 单核直接调用
/* >8 End of called on single lcore. */
/* clean up the EAL */
rte_eal_cleanup(); // ⑧ 清理
return 0;
}
| 行 | 代码 | 教学说明 |
|---|---|---|
| ① | rte_eal_init(argc, argv) |
标准 EAL 初始化(同 helloworld)。注意这里用了 rte_exit 而非 rte_panic——前者更优雅。 |
| ② | argc -= ret; argv += ret |
重要模式:EAL 会消费掉它认识的参数(如 -l、-a),ret 是消费的个数。减去后剩余参数可传给应用自己的解析逻辑(本示例没有应用层参数)。 |
| ③ | rte_eth_dev_count_avail() |
统计 EAL 初始化时探测到的可用网卡数量。本示例要求偶数个端口,因为端口配对转发(0↔1, 2↔3)。 |
| ④ | rte_pktmbuf_pool_create() |
创建 mbuf 专用的 mempool。NUM_MBUFS * nb_ports 按端口数放大总池容量。参数含义见下文 2.2 节。 |
| ⑤ | port_init(portid, mbuf_pool) |
对每个可用端口调用初始化函数。mbuf_pool 同一个池,所有端口共享——因为一个包只会被一个端口使用。 |
| ⑥ | rte_lcore_count() > 1 |
提醒用户:虽然你用 -l 开了多个 lcore,但我只用 main lcore 一个。因为每个端口只配了 1 个队列。 |
| ⑦ | lcore_main() |
直接在 main lcore 上调用(不用 remote_launch),因为这是单核应用。函数被标记为 __rte_noreturn,实际不会返回(死循环直到 Ctrl+C)。 |
| ⑧ | rte_eal_cleanup() |
实际执行不到(因为 lcore_main() 死循环),但写出来是标准范式。Ctrl+C 时进程被终止,OS 回收资源。 |
2. 核心概念深度解析
2.1 DPDK 网卡初始化的标准流程
这是整个例程中最需要牢记的部分——DPMK 网卡初始化的步骤是固定且有序的,顺序不能颠倒:
port_init(port, mbuf_pool)
│
▼
┌─────────────────────────┐
│ ① rte_eth_dev_is_valid │ 有效性检查
│ _port(port) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ② rte_eth_dev_info_get │ 读取硬件能力
│ (port, &dev_info) │ (offload、速度、队列限制...)
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ③ rte_eth_dev_configure │ 核心配置
│ (port, rx_n, tx_n, │ 指定队列数量 + 全局配置
│ &port_conf) │ 触发网卡驱动的内部初始化
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ④ adjust_nb_rx_tx_desc │ 校准描述符数量
│ (port, &nb_rxd, &nb_txd)│ 适配硬件限制
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ⑤ rte_eth_rx_queue_setup│ 为每个 RX 队列分配硬件环
│ (port, q, nb_rxd, │ + 绑定 mbuf_pool
│ socket, NULL, pool) │ (rx_rings 次循环)
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ⑥ rte_eth_tx_queue_setup│ 为每个 TX 队列分配硬件环
│ (port, q, nb_txd, │ (tx_rings 次循环)
│ socket, &txconf) │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ⑦ rte_eth_dev_start │ 启动网卡硬件
│ (port) │ 开始收发包
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ ⑧ promiscuous_enable │ 混杂模式
│ (port) │ 接收所有包(非本MAC也收)
└─────────────────────────┘
为什么这个顺序不能颠倒? 每一步都依赖上一步的结果——必须先 configure 才能 queue_setup,必须先 queue_setup 才能 start。
2.2 mbuf 与 mempool:数据包的"物流系统"
这是 DPDK 与 Linux 内核网络栈最大的不同之一。
┌─────────────────────────────────────────────────────────┐
│ rte_mempool │
│ "MBUF_POOL" │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ mbuf │ │ mbuf │ │ mbuf │ ... │ mbuf │ │
│ │ #0 │ │ #1 │ │ #2 │ │ #N-1 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
└─────────────────────────────────────────────────────────┘
│ │
│ rte_pktmbuf_alloc() rte_pktmbuf_free() │
▼ │
┌─────────┐ ┌─────────┐
│ RX 队列 │──收包──► 应用处理 ──发包──► │ TX 队列 │
│ (网卡写) │ (转发/修改) (网卡读) │ │
└─────────┘ └─────────┘
mbuf 的生命周期:
- 初始时所有 mbuf 都在 mempool 空闲列表中
- 网卡收到包 → 从 mempool 取 mbuf → DMA 写入数据 → 传给应用
- 应用处理后 →
rte_eth_tx_burst提交给 TX 队列 - 网卡发送完成后 → mbuf 自动释放回 mempool(如果开启了
MBUF_FAST_FREE) - 如果 TX 发不完 → 应用必须手动调用
rte_pktmbuf_free()释放
rte_pktmbuf_pool_create 参数详解:
mbuf_pool = rte_pktmbuf_pool_create(
"MBUF_POOL", // name: 池名称(调试用,多进程共享需要同名)
NUM_MBUFS * nb_ports, // n: 池中 mbuf 总数
MBUF_CACHE_SIZE, // cache_size: 每核本地缓存数(减少锁竞争)
0, // priv_size: 每个 mbuf 的私有数据区大小
RTE_MBUF_DEFAULT_BUF_SIZE, // data_room_size: mbuf 数据区大小(默认 2048 + headroom)
rte_socket_id() // socket_id: 在哪块 NUMA node 上分配内存
);
mbuf 内部结构(简化):
┌────────────┬──────────┬──────────────────────────────┐
│ rte_mbuf │ headroom │ data room │
│ (metadata) │ (128B) │ (RTE_MBUF_DEFAULT_BUF_SIZE) │
│ │ │ (默认 2048B) │
├────────────┼──────────┼──────────────────────────────┤
│ · port │ 留给用户 │ 实际网络包数据 │
│ · ol_flags │ 追加协议 │ (Ethernet/IP/TCP/UDP...) │
│ · pkt_len │ 头的空间 │ │
│ · data_off │ │ │
│ · next │ │ │
│ · nb_segs │ │ │
└────────────┴──────────┴──────────────────────────────┘
2.3 RX/TX Burst:DPDK 的批量收发包模型
为什么是 Burst(批量)?
传统 Linux 网络栈每次收一个包(recv())→ 一次系统调用 → 上下文切换 → 数据拷贝。DPDK 的思路是:
每轮处理 32 个包!
═══════════════
RX Ring ┌─┬─┬─┬─┬─┬─┬─┬─┬ 一次 rte_eth_rx_burst()
(硬件) │ │ │ │ │ │ │ │ │ → 批量读取所有就绪描述符
└─┴─┴─┴─┴─┴─┴─┴─┴ → 一次性填充 bufs[] 数组
┌─┬─┬─┬─┬─┬─┬─┬─┬ 一次 rte_eth_tx_burst()
TX Ring │ │ │ │ │ │ │ │ │ → 批量写入所有待发送描述符
(硬件) └─┴─┴─┴─┴─┴─┴─┴─┴ → 网卡硬件一次 DMA burst 取走
Burst Size 参数选择:
#define BURST_SIZE 32
| Burst Size | 延迟 | 吞吐 | CPU 效率 |
|---|---|---|---|
| 1 | 最低 | 最低 | 最低(每次函数调用只处理 1 个包) |
| 8 | 低 | 中 | 中 |
| 32 | 中 | 高 | 高(推荐起点) |
| 64 | 高 | 最高 | 最高(高负载时最优) |
| 128+ | 高 | 饱和 | 不再提升(受限于描述符环大小) |
32 是 DPDK 示例中的常见默认值,适合大多数场景。
RX Burst 的内部操作(rte_eth_rx_burst):
- 读取硬件 RX 描述符环的 head 指针,确认有多少新包到达
- 将已完成的描述符对应的 mbuf 指针复制到
bufs[]数组 - 补充新 mbuf:从 mempool 分配新 mbuf 填入空出的描述符位置(让网卡有地方写下一批包)
- 更新 tail 指针,通知硬件"这些位置可以用了"
- 返回实际收到的包数
TX Burst 的内部操作(rte_eth_tx_burst):
- 检查 TX 描述符环中有多少空闲位置
- 将要发送的 mbuf 指针填入空闲描述符
- 更新 tail 指针,触发硬件 DMA 操作
- 如果空闲位置不够,只发送能装下的包(返回值 < 请求值)
- 应用必须检查返回值,释放未发送的包!
2.4 端口配对与 NUMA 感知
端口配对:port ^ 1
const uint16_t nb_tx = rte_eth_tx_burst(port ^ 1, 0, bufs, nb_rx);
^ 是 XOR(异或)运算:
port = 0 → port ^ 1 = 1 (0 收到的发到 1)
port = 1 → port ^ 1 = 0 (1 收到的发到 0)
port = 2 → port ^ 1 = 3 (2 收到的发到 3)
port = 3 → port ^ 1 = 2 (3 收到的发到 2)
这是 DPDK 示例中惯用的**“端口配对转发”**模式。在实际场景中,端口 0 可能接内网,端口 1 接外网,形成简单的二层桥。
NUMA 警告(第 124~130 行):
NUMA Node 0 NUMA Node 1
┌─────────────────┐ ┌─────────────────┐
│ lcore 0 │ │ lcore 1 │
│ Port 0 (本地) │ │ Port 1 (本地) │
│ ↕ 低延迟 │ │ ↕ 低延迟 │
├─────────────────┤ ├─────────────────┤
│ Port 1 (远端) │ │ Port 0 (远端) │
│ ↕ 高延迟 ⚠️ │ │ ↕ 高延迟 ⚠️ │
└─────────────────┘ └─────────────────┘
如果在某 NUMA node 上的 lcore 操作了另一个 NUMA node 上的网卡,每次 DMA 都要跨 QPI/UPI 总线,内存带宽和延迟都会恶化。这个警告提醒你注意性能。
3. 编译与运行
3.1 编译
# 在 DPDK 源码根目录
cd dpdk-22.07
# 方式一:作为 DPDK 整体的一部分编译
meson setup build
cd build
meson configure -Dexamples=skeleton
ninja
# 可执行文件:build/examples/dpdk-basicfwd
# 方式二:独立 Makefile 编译
cd examples/skeleton
make
# 可执行文件:build/basicfwd
3.2 运行前提
- root 权限(需要操作 hugepage 和网卡硬件)
- hugepage 已挂载:
echo 512 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages - 驱动已开启:
cd /home/lbh/01_dpdk/dpdk-kmods/linux/igb_uio modprobe uio insmod igb_uio.ko intr_mode=legacy - 至少 2 个 DPDK 兼容网卡端口(且为偶数个),网卡需要绑定到 DPDK 驱动:
# 查看网卡 PCI 地址 dpdk-devbind.py --status # 绑定到 vfio-pci 或 igb_uio ifconfig ens38 down ifconfig ens39 down dpdk-devbind.py -b igb_uio 0000:02:06.0 0000:02:07.0
3.3 运行
# 使用 1 个 lcore,2 个网卡端口
sudo ./build/basicfwd -l 0 -a 0000:02:06.0 -a 0000:02:07.0
# 期望输出:
# Port 0 MAC: xx xx xx xx xx xx
# Port 1 MAC: xx xx xx xx xx xx
#
# Core 0 forwarding packets. [Ctrl+C to quit]
启动后,从端口 0 注入流量,端口 1 会原样输出(反之亦然),就是一个最简单的二层线速转发器。
3.4 不同参数下的行为
| 场景 | 行为 |
|---|---|
| 只指定 1 个端口 | 程序拒绝启动:number of ports must be even |
| 指定 3 个端口(奇数) | 同样拒绝(nb_ports & 1 检查) |
-l 0-3 开了 4 个 lcore |
警告 Too many lcores enabled. Only 1 used.,然后只用 main lcore 转发 |
| 指定 4 个端口 | 配对:0↔1, 2↔3,所有端口正常转发 |
4. 程序执行流程可视化
main() 开始
│
▼
┌──────────────────────┐
│ rte_eal_init(argc, │
│ argv) │
│ · hugepage 内存 │
│ · PCI 总线扫描 │
│ · lcore 绑定 │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ argc -= ret │
│ argv += ret │ 剥离 EAL 参数
└──────────────────────┘
│
▼
┌──────────────────────┐
│ nb_ports = │
│ rte_eth_dev_count_ │ 统计可用端口
│ avail() │
└──────────────────────┘
│
▼
nb_ports < 2 或奇数?
│ │
是 否
│ │
▼ ▼
rte_exit() ┌──────────────────────┐
│ rte_pktmbuf_pool_ │
│ create("MBUF_POOL", │
│ NUM_MBUFS*nb_ports, │
│ CACHE_SIZE, ...) │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ RTE_ETH_FOREACH_DEV │
│ port_init(port, │ 对每个端口:
│ mbuf_pool) │ · dev_configure
│ │ · rx/tx_queue_setup
│ │ · dev_start
│ │ · promiscuous_enable
└──────────────────────┘
│
▼
┌──────────────────────┐
│ lcore_main() │
│ │
│ for (;;) { │
│ RTE_ETH_FOREACH_DEV │ ◄── 无限循环
│ rx = rx_burst(p) │
│ if (rx == 0) │
│ continue; │
│ tx = tx_burst( │
│ p^1, bufs, rx) │
│ free unsent mbufs │
│ } │
└──────────────────────┘
│
(永不执行)
▼
rte_eal_cleanup()
数据包处理时间线(单个 lcore 轮询 2 个端口):
═══════════════════════════════════════════════════════════
时间 →
─────────────────────────────────────────────────────────
检查 Port 0 ──→ 收到 32 个包 ──→ 发往 Port 1 ──→ 释放未发送
检查 Port 1 ──→ 收到 16 个包 ──→ 发往 Port 0 ──→ 全部成功
检查 Port 0 ──→ 无包 → continue
检查 Port 1 ──→ 收到 8 个包 ──→ 发往 Port 0 ──→ 全部成功
... (持续轮询)
═══════════════════════════════════════════════════════════
整体涉及到底层的流程如下:
5. 与其他例程的关系
skeleton 是 DPDK 网络编程的"骨架",后面所有涉及网卡收发包的例程都在此基础上扩展:
| 维度 | helloworld | skeleton (basicfwd) | symmetric_mp | client_server_mp |
|---|---|---|---|---|
| EAL 初始化 | ✅ | ✅ | ✅ | ✅ |
| mbuf / mempool | ❌ | ✅(创建+使用) | ✅ | ✅ |
| 网卡端口初始化 | ❌ | ✅(标准流程) | ✅ | ✅ |
| RX/TX Burst 收发 | ❌ | ✅(单核单队列) | ✅(多核多队列) | ✅(C/S 分发) |
| 多核 | ✅(仅打印) | ❌(单核) | ✅ | ✅ |
| 多进程 | ❌ | ❌ | ✅ | ✅ |
| NUMA 感知 | ❌ | ✅(警告) | ❌ | ❌ |
递进关系:
helloworld 只学 EAL + lcore(纯 CPU 操作)
│
▼
skeleton (basicfwd) + mbuf + 网卡初始化 + RX/TX Burst(数据包操作)
│
▼
symmetric_mp + 多核多队列 + 多进程(扩展并行度)
│
▼
client_server_mp + ring 通信 + C/S 架构 + memzone(进程间解耦)
skeleton 中的 port_init 函数被后续几乎所有示例直接复用或稍作修改,这就是为什么它叫"骨架"。
6. 常见面试问题
Q1: DPDK 网卡初始化必须遵循什么顺序?为什么?
答:标准顺序为 dev_info_get → dev_configure → adjust_nb_rx_tx_desc → rx/tx_queue_setup → dev_start → promiscuous_enable。顺序不能颠倒,因为 configure 依赖 info_get 的能力信息,queue_setup 依赖 configure 的队列数量配置,start 依赖 queue_setup 完成了硬件资源分配。
Q2: 为什么 port_init 中的 rx_rings 和 tx_rings 都设为 1?
答:因为 skeleton 是单核应用,每个端口只需 1 个 RX 队列和 1 个 TX 队列。队列数代表了可以有多少个 lcore 同时操作同一个端口:每个 lcore 独占自己的队列。如果需要多核并行收发包(如 symmetric_mp),就需要为每个 lcore 创建独立的队列。
Q3: rte_eth_tx_burst 返回值小于请求数意味着什么?必须怎么处理?
答:意味着 TX 描述符环中没有足够的空闲位置来容纳所有待发送包。必须调用 rte_pktmbuf_free() 释放未发送的 mbuf,否则这些 mbuf 永远不会回到 mempool,最终导致池空、网卡无 buffer 可用而丢包。
Q4: mbuf pool 的大小 NUM_MBUFS * nb_ports 是如何估算的?
答:需要覆盖的 mbuf 数量 = RX Ring 中的(等待写入的)+ 应用中正在处理的 + TX Ring 中的(等待发送的)+ lcore 缓存中的。8191 × nb_ports 是一个经验值。如果太小,会出现"池空→网卡无 buffer→丢包";如果太大,浪费 hugepage 内存。
Q5: unlikely() 宏在这里有什么用?可以去掉吗?
答:unlikely(nb_rx == 0) 告诉 CPU 的分支预测器"这个条件大概率为 false"(即多数时候能收到包)。可以去掉,逻辑上无影响,但在高流量场景下,正确的分支预测能减少 CPU 流水线冲刷,显著提升性能。
Q6: 这个例程用了几个 lcore?如果我想用多个 lcore 怎么办?
答:只用了 1 个(main lcore 直接调用 lcore_main())。要用多 lcore,需要:
- 把每个端口的
rx_rings/tx_rings改成和 lcore 数量相同 - 用
rte_eal_remote_launch把lcore_main分发到多个 lcore - 每个 lcore 使用不同的队列 ID(queue_id = lcore_index),避免多核竞争同一队列
Q7: port ^ 1 这种端口配对方式的局限性?
答:只适用于偶数端口数量和简单的 1:1 配对场景。如果端口数不是 2 的倍数或需要更复杂的转发逻辑(如基于 MAC/VLAN/IP 的路由),就需要查表或查哈希。symmetric_mp 使用了相同的配对方式,client_server_mp 则使用了更灵活的查表方式。
7. 延伸阅读
- DPDK 官方文档 - Skeleton Sample App
- DPDK Programmer’s Guide - Ethernet Device API
- DPDK Programmer’s Guide - Mbuf Library
- DPDK Programmer’s Guide - Mempool Library
- DPDK API Reference - rte_ethdev.h
- DPDK API Reference - rte_mbuf.h
- 同目录下的
helloworld/(EAL + lcore 基础) - 同目录下的
multi_process/symmetric_mp/(多核多进程扩展版骨架转发)
8. 本示例涉及的 API 总结
8.1 API 速查表
| # | API | 类型 | 所属头文件 | 功能说明 |
|---|---|---|---|---|
| 1 | rte_eal_init(argc, argv) |
函数 | <rte_eal.h> |
EAL 初始化。解析命令行参数,分配 hugepage,扫描 PCI 总线,绑定 lcore。返回 EAL 处理的参数个数。 |
| 2 | rte_eal_cleanup() |
函数 | <rte_eal.h> |
清理 EAL 资源。释放 hugepage 映射,恢复 CPU 亲和性。 |
| 3 | rte_exit(code, fmt, ...) |
函数 | <rte_eal.h> |
格式化打印错误信息后退出程序。比 rte_panic 更温和(不 dump 堆栈)。 |
| 4 | rte_lcore_id() |
函数 | <rte_lcore.h> |
获取当前 lcore ID。 |
| 5 | rte_lcore_count() |
函数 | <rte_lcore.h> |
获取可用的 lcore 总数。 |
| 6 | rte_socket_id() |
函数 | <rte_lcore.h> |
获取当前 lcore 所在的 NUMA node ID。 |
| 7 | rte_eth_dev_count_avail() |
函数 | <rte_ethdev.h> |
获取 EAL 初始化后探测到的可用以太网端口总数。 |
| 8 | rte_eth_dev_is_valid_port(port) |
函数 | <rte_ethdev.h> |
检查端口 ID 是否对应一个有效的网卡设备。 |
| 9 | rte_eth_dev_info_get(port, &info) |
函数 | <rte_ethdev.h> |
获取网卡硬件能力信息(offload 特性、速度范围、队列限制等)。dev_configure 前必须调用。 |
| 10 | rte_eth_dev_configure(port, rx_n, tx_n, &conf) |
函数 | <rte_ethdev.h> |
配置网卡端口:指定 RX/TX 队列数量和全局配置(offload、RSS 等)。触发网卡驱动的内部初始化。 |
| 11 | rte_eth_dev_adjust_nb_rx_tx_desc(port, &rxd, &txd) |
函数 | <rte_ethdev.h> |
将请求的描述符环大小调整为网卡硬件实际支持的值。 |
| 12 | rte_eth_rx_queue_setup(port, q, nb_rxd, socket, NULL, pool) |
函数 | <rte_ethdev.h> |
分配并初始化指定端口上的一个 RX 队列:分配硬件描述符环,绑定 mbuf pool(网卡从此池取 mbuf 写数据)。 |
| 13 | rte_eth_tx_queue_setup(port, q, nb_txd, socket, &txconf) |
函数 | <rte_ethdev.h> |
分配并初始化指定端口上的一个 TX 队列:分配硬件描述符环,设置 offload 参数。 |
| 14 | rte_eth_dev_start(port) |
函数 | <rte_ethdev.h> |
启动网卡端口:开启 RX/TX 引擎,开始收发数据包。必须在队列 setup 之后调用。 |
| 15 | rte_eth_promiscuous_enable(port) |
函数 | <rte_ethdev.h> |
开启端口混杂模式:接收所有经过的帧(不限于目标 MAC 是本端口)。转发设备必须开启。 |
| 16 | rte_eth_macaddr_get(port, &addr) |
函数 | <rte_ethdev.h> |
读取网卡端口的 MAC 地址。 |
| 17 | rte_eth_dev_socket_id(port) |
函数 | <rte_ethdev.h> |
获取网卡所在的 NUMA node ID,用于 NUMA 感知的性能优化。 |
| 18 | rte_eth_rx_burst(port, queue, bufs, nb_pkts) |
函数 | <rte_ethdev.h> |
核心:从指定端口的指定队列批量收包。最多收 nb_pkts 个,返回实际收到的包数。内部自动补充新 mbuf 到 RX 环。 |
| 19 | rte_eth_tx_burst(port, queue, bufs, nb_pkts) |
函数 | <rte_ethdev.h> |
核心:向指定端口的指定队列批量发包。尽力而为——如果 TX 环空闲不够,返回值会 < nb_pkts。应用必须释放未发送的 mbuf。 |
| 20 | rte_pktmbuf_pool_create(name, n, cache, priv, data_size, socket) |
函数 | <rte_mbuf.h> |
创建 mbuf 专用的 mempool。参数包含池大小、每核缓存、数据区大小、NUMA node。 |
| 21 | rte_pktmbuf_free(m) |
函数 | <rte_mbuf.h> |
释放一个 mbuf 回 mempool。TX burst 发送失败的包必须手动调用此函数释放。 |
| 22 | RTE_ETH_FOREACH_DEV(port) |
宏 | <rte_ethdev.h> |
遍历所有已探测的网卡端口 ID。类似 RTE_LCORE_FOREACH_WORKER。 |
| 23 | unlikely(expr) |
宏 | <rte_branch_prediction.h> |
分支预测提示:告诉 CPU 该分支"不太可能发生",用于优化指令流水线。 |
| 24 | __rte_noreturn |
宏 | <rte_common.h> |
标记函数永不返回(如死循环),帮助编译器优化和静态分析。 |
8.2 按调用顺序的调用关系图
main()
│
├─ [1] rte_eal_init(argc, argv)
│ └─ 失败 → [3] rte_exit(...)
│
├─ [7] rte_eth_dev_count_avail() ← 检查端口数
│
├─ [6] rte_socket_id()
├─ [20] rte_pktmbuf_pool_create(...) ← 创建 mbuf 池
│
├─ [22] RTE_ETH_FOREACH_DEV(port)
│ └─ port_init(port, mbuf_pool)
│ ├─ [8] rte_eth_dev_is_valid_port()
│ ├─ [9] rte_eth_dev_info_get()
│ ├─ [10] rte_eth_dev_configure()
│ ├─ [11] rte_eth_dev_adjust_nb_rx_tx_desc()
│ ├─ [12] rte_eth_rx_queue_setup()
│ ├─ [13] rte_eth_tx_queue_setup()
│ ├─ [14] rte_eth_dev_start()
│ ├─ [16] rte_eth_macaddr_get()
│ └─ [15] rte_eth_promiscuous_enable()
│
├─ [5] rte_lcore_count() ← 多核检查
│
└─ lcore_main() ← 死循环转发
│
├─ [22] RTE_ETH_FOREACH_DEV(port) ← [17] rte_eth_dev_socket_id() NUMA 检查
│
└─ for (;;)
└─ [22] RTE_ETH_FOREACH_DEV(port)
├─ [18] rte_eth_rx_burst(port, 0, bufs, 32)
├─ if (nb_rx == 0) [23] unlikely(...) → continue
├─ [19] rte_eth_tx_burst(port^1, 0, bufs, nb_rx)
└─ if (nb_tx < nb_rx) [21] rte_pktmbuf_free()
│
└─ (永不执行) [2] rte_eal_cleanup()
8.3 API 分类
| 分类 | API | 用途场景 |
|---|---|---|
| 生命周期 | rte_eal_init → … → rte_eal_cleanup |
任意 DPDK 程序的标准初始化/清理骨架 |
| 端口配置 | rte_eth_dev_* 系列(info_get, configure, adjust, queue_setup×2, start, promiscuous_enable, macaddr_get) |
网卡初始化的标准 8 步流程 |
| 数据包收发 | rte_eth_rx_burst, rte_eth_tx_burst |
高性能批量收发,DPDK 网络应用的核心 |
| 内存管理 | rte_pktmbuf_pool_create, rte_pktmbuf_free |
mbuf 池创建 + 发送失败的包回收 |
| 设备遍历 | RTE_ETH_FOREACH_DEV, rte_eth_dev_count_avail, rte_eth_dev_is_valid_port |
遍历和筛选可用端口 |
| NUMA 感知 | rte_socket_id, rte_eth_dev_socket_id |
检查 lcore 与网卡的 NUMA 亲和性 |
| 编译优化 | unlikely, __rte_noreturn |
分支预测提示 + 永不返回标记 |
下一步学习建议:skeleton 是 DPDK 网络编程的基石。理解
port_init的每个步骤和rte_eth_rx/tx_burst的批量模型后,建议学习multi_process/symmetric_mp,看同一个骨架如何扩展为多核多进程转发,以及l3fwd(三层转发),看如何在 skeleton 基础上加入查表路由逻辑。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)