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_rxdnb_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 专用的 mempoolNUM_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 的生命周期

  1. 初始时所有 mbuf 都在 mempool 空闲列表中
  2. 网卡收到包 → 从 mempool 取 mbuf → DMA 写入数据 → 传给应用
  3. 应用处理后 → rte_eth_tx_burst 提交给 TX 队列
  4. 网卡发送完成后 → mbuf 自动释放回 mempool(如果开启了 MBUF_FAST_FREE
  5. 如果 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):

  1. 读取硬件 RX 描述符环的 head 指针,确认有多少新包到达
  2. 将已完成的描述符对应的 mbuf 指针复制到 bufs[] 数组
  3. 补充新 mbuf:从 mempool 分配新 mbuf 填入空出的描述符位置(让网卡有地方写下一批包)
  4. 更新 tail 指针,通知硬件"这些位置可以用了"
  5. 返回实际收到的包数

TX Burst 的内部操作rte_eth_tx_burst):

  1. 检查 TX 描述符环中有多少空闲位置
  2. 将要发送的 mbuf 指针填入空闲描述符
  3. 更新 tail 指针,触发硬件 DMA 操作
  4. 如果空闲位置不够,只发送能装下的包(返回值 < 请求值)
  5. 应用必须检查返回值,释放未发送的包!

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_getdev_configureadjust_nb_rx_tx_descrx/tx_queue_setupdev_startpromiscuous_enable。顺序不能颠倒,因为 configure 依赖 info_get 的能力信息,queue_setup 依赖 configure 的队列数量配置,start 依赖 queue_setup 完成了硬件资源分配。

Q2: 为什么 port_init 中的 rx_ringstx_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,需要:

  1. 把每个端口的 rx_rings/tx_rings 改成和 lcore 数量相同
  2. rte_eal_remote_launchlcore_main 分发到多个 lcore
  3. 每个 lcore 使用不同的队列 ID(queue_id = lcore_index),避免多核竞争同一队列

Q7: port ^ 1 这种端口配对方式的局限性?

:只适用于偶数端口数量简单的 1:1 配对场景。如果端口数不是 2 的倍数或需要更复杂的转发逻辑(如基于 MAC/VLAN/IP 的路由),就需要查表或查哈希。symmetric_mp 使用了相同的配对方式,client_server_mp 则使用了更灵活的查表方式。


7. 延伸阅读


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 基础上加入查表路由逻辑。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐