【DPDK例程学习】(4) l2fwd

DPDK l2fwd(二层转发)例程详解教程

学习目标:通过 DPDK 经典二层转发示例,掌握多 lcore 端口分配、TX Buffer 批量优化、MAC 地址重写、TSC 定时器统计打印,以及从 skeleton 骨架到工业级应用的演进路径。


目录

  • [1. 源码逐行解读](#1. 源码逐行解读)
    • [1.1 头文件、宏定义与全局数据(第 1~117 行)](#1.1 头文件、宏定义与全局数据(第 1~117 行))
    • [1.2 统计打印与 MAC 重写(第 119~200 行)](#1.2 统计打印与 MAC 重写(第 119~200 行))
    • [1.3 主循环 l2fwd_main_loop(第 202~299 行)](#1.3 主循环 l2fwd_main_loop(第 202~299 行))
    • [1.4 参数解析与端口配对(第 308~573 行)](#1.4 参数解析与端口配对(第 308~573 行))
    • [1.5 主函数 main(第 649~932 行)](#1.5 主函数 main(第 649~932 行))
  • [2. 核心概念深度解析](#2. 核心概念深度解析)
    • [2.1 TX Buffer:批量发送的"中间缓冲区"](#2.1 TX Buffer:批量发送的"中间缓冲区")
    • [2.2 lcore 到端口的分配策略](#2.2 lcore 到端口的分配策略)
    • [2.3 MAC 地址重写机制](#2.3 MAC 地址重写机制)
    • [2.4 TSC 定时器统计 + 优雅退出](#2.4 TSC 定时器统计 + 优雅退出)
  • [3. 编译与运行](#3. 编译与运行)
  • [4. 程序执行流程可视化](#4. 程序执行流程可视化)
  • [5. 与其他例程的关系](#5. 与其他例程的关系)
  • [6. 常见面试问题](#6. 常见面试问题)
  • [7. 延伸阅读](#7. 延伸阅读)
  • [8. 本示例涉及的 API 总结](#8. 本示例涉及的 API 总结)

1. 源码逐行解读

l2fwd 是 DPDK 的"教科书级"示例,933 行代码覆盖了从端口分配、TX Buffer、MAC 重写到信号处理的完整生产级流程。它是 skeleton 的直接进阶版。

1.1 头文件、宏定义与全局数据(第 1~117 行)

c 复制代码
// === 头文件 ===
#include <rte_common.h>      // 通用宏
#include <rte_malloc.h>      // ① rte_zmalloc_socket
#include <rte_memcpy.h>      // ② rte_memcpy
#include <rte_prefetch.h>    // ③ rte_prefetch0
#include <rte_random.h>      // 随机数
#include <rte_ether.h>       // ④ 以太网帧结构体 rte_ether_hdr / rte_ether_addr
#include <rte_string_fns.h>  // 字符串工具

// === 全局配置 ===
static volatile bool force_quit;                     // ⑤ Ctrl+C 标志
static int mac_updating = 1;                         // ⑥ MAC 重写开关
static int promiscuous_on;                           // ⑦ 混杂模式开关

#define MAX_PKT_BURST 32                             // ⑧ Burst 大小
#define BURST_TX_DRAIN_US 100                        // ⑨ TX Buffer 刷新间隔 (100µs)
#define MEMPOOL_CACHE_SIZE 256                       // ⑩ mempool 缓存

static uint16_t nb_rxd = 1024;                       // ⑪ 可配置描述符数量
static uint16_t nb_txd = 1024;

static struct rte_ether_addr l2fwd_ports_eth_addr[RTE_MAX_ETHPORTS];  // ⑫ 端口 MAC
static uint32_t l2fwd_enabled_port_mask = 0;         // ⑬ 启用端口位掩码
static uint32_t l2fwd_dst_ports[RTE_MAX_ETHPORTS];   // ⑭ 目标端口映射表

// === 端口配对参数 ===
struct port_pair_params {                            // ⑮ 自定义端口对
    #define NUM_PORTS 2
    uint16_t port[NUM_PORTS];
} __rte_cache_aligned;
static struct port_pair_params port_pair_params_array[RTE_MAX_ETHPORTS / 2];

static unsigned int l2fwd_rx_queue_per_lcore = 1;    // ⑯ 每 lcore 负责的队列数

// === lcore 到端口队列的映射 ===
struct lcore_queue_conf {                            // ⑰ 核心数据结构!
    unsigned n_rx_port;                              // 该 lcore 负责的端口数
    unsigned rx_port_list[MAX_RX_QUEUE_PER_LCORE];   // 端口列表
} __rte_cache_aligned;
struct lcore_queue_conf lcore_queue_conf[RTE_MAX_LCORE];

static struct rte_eth_dev_tx_buffer *tx_buffer[RTE_MAX_ETHPORTS];  // ⑱ TX Buffer 数组

// === 统计与定时器 ===
struct l2fwd_port_statistics {
    uint64_t tx;
    uint64_t rx;
    uint64_t dropped;
} __rte_cache_aligned;
static struct l2fwd_port_statistics port_statistics[RTE_MAX_ETHPORTS];

static uint64_t timer_period = 10;                  // ⑲ 统计打印周期 (默认 10s)
代码 教学说明
<rte_malloc.h> DPDK 的 rte_zmalloc_socket:在指定 NUMA node 上分配清零内存,用于 TX Buffer。
<rte_prefetch.h> 预取指令rte_prefetch0() 将数据提前加载到 L1 Cache,减少处理 mbuf 时的 Cache Miss。这是本示例相比 skeleton 新增的性能优化技术
<rte_ether.h> 以太网协议结构体:rte_ether_hdr(MAC 头)、rte_ether_addr(MAC 地址)、RTE_ETHER_ADDR_BYTES 等。
force_quit volatile bool,由信号处理器设置,实现优雅退出。skeleton 只能用 Ctrl+C 强杀,l2fwd 可以捕获信号后完成清理再退出。
BURST_TX_DRAIN_US = 100 TX Buffer 的定时刷新间隔(微秒)。不是每个包都立即发,而是攒一批或每隔 100µs 才 flush,减少 PCIe 事务次数。
l2fwd_dst_ports[port] 核心映射表l2fwd_dst_ports[2] = 3 表示"从端口 2 收到的包转发到端口 3"。支持默认的交替配对(0↔1, 2↔3)和用户自定义配对。
lcore_queue_conf 本示例最核心的数据结构 。将每个启用的网卡端口分配给特定的 lcorelcore_queue_conf[3].rx_port_list = {0, 2} 表示 lcore 3 负责轮询端口 0 和 2。
tx_buffer[port] 每个端口一个 TX Buffer。TX Buffer 是 DPDK 提供的软件批量缓冲 ,将零散的 rte_eth_tx_burst 调用聚合为更大的批次。
timer_period = 10 统计打印间隔(秒),之后在 main 中转换为 TSC cycles。可调整为 0(禁用)到 86400(1 天)。

1.2 统计打印与 MAC 重写(第 119~200 行)

c 复制代码
static void
print_stats(void)
{
    // ...
    const char clr[] = { 27, '[', '2', 'J', '\0' };           // ① ANSI 清屏
    const char topLeft[] = { 27, '[', '1', ';', '1', 'H','\0' };  // ② 光标归位
    printf("%s%s", clr, topLeft);
    // ...
    for (portid = 0; portid < RTE_MAX_ETHPORTS; portid++) {   // ③ 遍历端口打印
        // ...
    }
    printf("...Aggregate statistics...");                      // ④ 汇总统计
}
教学说明
①② ANSI Escape 码\x1b[2J 清屏,\x1b[1;1H 光标移到 (1,1)。效果:每次打印统计时覆盖刷新而非刷屏,形成"实时仪表盘"。
逐端口打印 TX/RX/Drop 统计
汇总所有端口的总统计
MAC 重写 l2fwd_mac_updating(第 166~180 行)
c 复制代码
static void
l2fwd_mac_updating(struct rte_mbuf *m, unsigned dest_portid)
{
    struct rte_ether_hdr *eth;
    void *tmp;

    eth = rte_pktmbuf_mtod(m, struct rte_ether_hdr *);       // ① 获取以太网头指针

    /* 02:00:00:00:00:xx */
    tmp = &eth->dst_addr.addr_bytes[0];                      // ②
    *((uint64_t *)tmp) = 0x000000000002 +                     // ③ 构造 dst MAC
                          ((uint64_t)dest_portid << 40);

    /* src addr */
    rte_ether_addr_copy(&l2fwd_ports_eth_addr[dest_portid],  // ④ 复制 src MAC
                        &eth->src_addr);
}
教学说明
rte_pktmbuf_mtod() 将 mbuf 的数据区指针转换为以太网头指针。这是 mbuf 编程中最常用的宏之一。
②③ 目的 MAC 构造 :生成 02:00:00:00:00:XX 格式,XX = dest_portid。巧妙利用字节序------0x000000000002 占了低 6 字节中的前 5 字节(02:00:00:00:00),dest_portid << 40 将端口号移入第 6 字节。
源 MAC 替换:将发送端口的真实 MAC 地址写入源 MAC 字段。接收方看到的是"包来自这个端口"。
转发入口 l2fwd_simple_forward(第 182~200 行)
c 复制代码
static void
l2fwd_simple_forward(struct rte_mbuf *m, unsigned portid)
{
    unsigned dst_port;
    int sent;
    struct rte_eth_dev_tx_buffer *buffer;

    dst_port = l2fwd_dst_ports[portid];                     // ① 查表获取目标端口

    if (mac_updating)
        l2fwd_mac_updating(m, dst_port);                    // ② 可选 MAC 重写

    buffer = tx_buffer[dst_port];
    sent = rte_eth_tx_buffer(dst_port, 0, buffer, m);       // ③ TX Buffer 缓冲发送
    if (sent)
        port_statistics[dst_port].tx += sent;               // ④ 统计更新
}
教学说明
查表转发l2fwd_dst_ports[portid] 是预先配置好的端口映射。不像 skeleton 用 port ^ 1 硬编码配对,这里支持灵活的端口映射。
rte_eth_tx_buffer() 替代了 skeleton 的 rte_eth_tx_burst()。它不立即发送,而是先缓冲,攒够一批或到时间了再 flush。

1.3 主循环 l2fwd_main_loop(第 202~299 行)

这是本示例最复杂的函数,分为三个阶段:

c 复制代码
static void
l2fwd_main_loop(void)
{
    // ... 变量声明 ...

    const uint64_t drain_tsc = (rte_get_tsc_hz() + US_PER_S - 1) / US_PER_S  // ①
                                * BURST_TX_DRAIN_US;                            //  100µs→cycles

    lcore_id = rte_lcore_id();
    qconf = &lcore_queue_conf[lcore_id];                      // ② 获取本核的队列配置

    if (qconf->n_rx_port == 0) {
        RTE_LOG(INFO, L2FWD, "lcore %u has nothing to do\n", lcore_id);     // ③ 空闲核退出
        return;
    }

    // 打印本核负责的端口
    for (i = 0; i < qconf->n_rx_port; i++)
        RTE_LOG(INFO, L2FWD, " -- lcoreid=%u portid=%u\n", lcore_id,
                qconf->rx_port_list[i]);

    while (!force_quit) {                                     // ④ 可退出的主循环!

        cur_tsc = rte_rdtsc();                                // ⑤ 读 TSC

        // ========== 阶段 A:TX Buffer 定时刷新 + 统计 ==========
        diff_tsc = cur_tsc - prev_tsc;
        if (unlikely(diff_tsc > drain_tsc)) {                // ⑥ 每 100µs 才执行一次

            for (i = 0; i < qconf->n_rx_port; i++) {         // ⑦ 刷新所有 TX Buffer
                portid = l2fwd_dst_ports[qconf->rx_port_list[i]];
                buffer = tx_buffer[portid];
                sent = rte_eth_tx_buffer_flush(portid, 0, buffer);
                if (sent)
                    port_statistics[portid].tx += sent;
            }

            if (timer_period > 0) {                          // ⑧ TSC 定时器统计
                timer_tsc += diff_tsc;
                if (unlikely(timer_tsc >= timer_period)) {
                    if (lcore_id == rte_get_main_lcore())    // ⑨ 只在 main lcore 打印
                        print_stats();
                    timer_tsc = 0;
                }
            }
            prev_tsc = cur_tsc;
        }

        // ========== 阶段 B:收包 + 转发 ==========
        for (i = 0; i < qconf->n_rx_port; i++) {            // ⑩ 遍历本核负责的端口
            portid = qconf->rx_port_list[i];
            nb_rx = rte_eth_rx_burst(portid, 0, pkts_burst, MAX_PKT_BURST);

            port_statistics[portid].rx += nb_rx;

            for (j = 0; j < nb_rx; j++) {                   // ⑪ 逐包处理
                m = pkts_burst[j];
                rte_prefetch0(rte_pktmbuf_mtod(m, void *)); // ⑫ 预取下一个包的头部
                l2fwd_simple_forward(m, portid);            // ⑬ 查表+MAC重写+缓冲发送
            }
        }
    }
}
教学说明
drain_tsc 计算:(hz + US_PER_S - 1) / US_PER_S 是向上取整得到 1µs 的 cycles 数,再乘以 100 得到 100µs。这比 timer 示例的 hz * 10 / 1000 更精确。
qconf 的妙处:每个 lcore 通过 lcore_id 找到自己负责的端口列表,互不干扰。这是多核 DPDK 的标准设计模式。
如果某 lcore 没有被分配到任何端口,它直接退出,不占 CPU。
while (!force_quit) 替代 skeleton 的 for(;;),支持优雅退出。
两阶段跳帧 :TX flush + 统计每 100µs 才执行一次,但收包每次都执行。这是因为收包不能延迟(会丢包),但发 flush 可以攒一批再发。
TX Buffer Flush :与 skeleton 不同,l2fwd 不是立即 rte_eth_tx_burst,而是先用 rte_eth_tx_buffer 缓冲,然后每 100µs 调用 rte_eth_tx_buffer_flush 强制发送。
只在 main lcore 打印:避免多个核同时输出统计导致的乱码和性能干扰。
rte_prefetch0()预取下一个包的以太网头部到 L1 Cache 。因为在下一个循环迭代中需要访问 m 的数据,提前预取可以显著减少 Cache Miss 延迟。这是 DPDK 高性能的典型技巧。

1.4 参数解析与端口配对(第 308~573 行)

l2fwd 支持丰富的命令行参数:

参数 功能 示例
-p PORTMASK 启用的端口位掩码(十六进制) -p 0x3 = 端口 0 和 1
-P 开启混杂模式 -P
-q NQ 每 lcore 负责的 RX 队列数(默认 1) -q 2
-T PERIOD 统计打印周期(秒,0 禁用,默认 10) -T 5
--no-mac-updating 禁用 MAC 地址重写 --no-mac-updating
--portmap="(0,2)(1,3)" 自定义端口配对 (port_src, port_dst)

端口配对逻辑(第 706~734 行):

复制代码
默认模式(无 --portmap):
  启用的端口按顺序配对: 0↔1, 2↔3, 4↔5, ...
  如果端口数为奇数,最后一个端口自环(发给自己)

--portmap 模式:
  --portmap="(0,2)(1,3)"  →  port0↔port2, port1↔port3

1.5 主函数 main(第 649~932 行)

c 复制代码
int
main(int argc, char **argv)
{
    // ========== 阶段 A:EAL + 参数 ==========
    ret = rte_eal_init(argc, argv);                        // ①
    force_quit = false;
    signal(SIGINT, signal_handler);                        // ② 注册信号处理器
    signal(SIGTERM, signal_handler);
    l2fwd_parse_args(argc, argv);                          // ③ 解析应用参数
    timer_period *= rte_get_timer_hz();                    // ④ 秒 → TSC cycles

    // ========== 阶段 B:lcore ←→ 端口 分配 ==========
    // 遍历启用的端口,轮询分配给各 lcore:
    RTE_ETH_FOREACH_DEV(portid) {
        // ...
        while (当前 lcore 已满 || 未启用)
            rx_lcore_id++;                                 // ⑤ 找下一个可用 lcore
        qconf->rx_port_list[qconf->n_rx_port] = portid;   // ⑥ 分配端口到 lcore
        qconf->n_rx_port++;
    }

    // ========== 阶段 C:mbuf pool 创建 ==========
    nb_mbufs = RTE_MAX(nb_ports * (nb_rxd + nb_txd + MAX_PKT_BURST +
                    nb_lcores * MEMPOOL_CACHE_SIZE), 8192U);  // ⑦ 动态计算池大小
    l2fwd_pktmbuf_pool = rte_pktmbuf_pool_create(...);

    // ========== 阶段 D:端口初始化 + TX Buffer ==========
    RTE_ETH_FOREACH_DEV(portid) {
        // 标准端口初始化(同 skeleton)
        rte_eth_dev_configure(portid, 1, 1, ...);
        rte_eth_rx_queue_setup(portid, 0, ...);
        rte_eth_tx_queue_setup(portid, 0, ...);

        // TX Buffer 的创建和初始化(skeleton 中没有!)
        tx_buffer[portid] = rte_zmalloc_socket("tx_buffer",
                RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST), ...);
        rte_eth_tx_buffer_init(tx_buffer[portid], MAX_PKT_BURST);
        rte_eth_tx_buffer_set_err_callback(tx_buffer[portid],
                rte_eth_tx_buffer_count_callback,
                &port_statistics[portid].dropped);            // ⑧ 发送失败→drop 计数

        rte_eth_dev_start(portid);
    }

    // ========== 阶段 E:广播主循环 + 等待退出 ==========
    rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MAIN);  // ⑨
    RTE_LCORE_FOREACH_WORKER(lcore_id) {
        rte_eal_wait_lcore(lcore_id);                        // ⑩ 逐个等待退出
    }

    // ========== 阶段 F:清理 ==========
    RTE_ETH_FOREACH_DEV(portid) {
        rte_eth_dev_stop(portid);                            // ⑪ 停止网卡
        rte_eth_dev_close(portid);                           // ⑫ 关闭网卡
    }
    rte_eal_cleanup();
}
教学说明
信号处理SIGINT(Ctrl+C) 和 SIGTERM 触发 signal_handler,设置 force_quit = true。这是优雅退出的起点。
⑤⑥ lcore-端口分配算法 :轮询遍历启用的端口,依次分配给你启用的 lcore。如果 -q 2,每个 lcore 可以分配 2 个端口。
mbuf 池大小动态计算 :不再用 skeleton 的固定 NUM_MBUFS * nb_ports,而是:nb_ports × (RX描述符 + TX描述符 + Burst最大) + nb_lcores × Cache,至少 8192。这是工业级的计算方法。
rte_eth_tx_buffer_set_err_callback 注册 TX 发送失败的回调函数,当 TX Buffer flush 时有包发送失败,自动调用 rte_eth_tx_buffer_count_callback 统计 drop 数量。
CALL_MAINrte_eal_mp_remote_launch(..., CALL_MAIN)CALL_MAIN 标志表示 main lcore 也要执行 l2fwd_launch_one_lcore。与 helloworld 中手动调用 lcore_hello(NULL) 效果类似,但这里更统一。
rte_eal_wait_lcore(lcore_id):等待单个 lcore 退出(而非 rte_eal_mp_wait_lcore 等所有),因为信号后各核可能不同时退出。
⑪⑫ 停止 + 关闭网卡。skeleton 中不需要(因为死循环永不退出),l2fwd 因为有优雅退出,需要完整清理。

2. 核心概念深度解析

2.1 TX Buffer:批量发送的"中间缓冲区"

这是 l2fwd 相比 skeleton 最重要的升级之一。

skeleton 的方式 :收到包 → 立即 rte_eth_tx_burst → 每个包都需要一次 PCIe 写操作来更新 TX 描述符。

l2fwd 的方式 :收到包 → rte_eth_tx_buffer(软件缓冲)→ 攒到 32 个或超过 100µs → rte_eth_tx_buffer_flush 一次性发出。

复制代码
    直接 TX Burst (skeleton)               TX Buffer (l2fwd)
    ════════════════════════               ═══════════════════
    
    每收一个包:                             每收一个包:
    ┌──────────┐                           ┌──────────┐
    │ 收到 mbuf │                           │ 收到 mbuf │
    └──────────┘                           └──────────┘
         │                                       │
         ▼                                       ▼
    rte_eth_tx_burst()                     rte_eth_tx_buffer()
    每次 PCIe 写                              写入软件缓冲区
         │                                  (几乎零开销!)    
         ▼                                       │
    ┌──────────┐                              [攒够32个或超时]
    │ 收到 mbuf │                                   │
    └──────────┘                                   ▼
         │                               rte_eth_tx_buffer_flush()
         ▼                               ← 一次 PCIe 写,发 32 个包
    rte_eth_tx_burst()
    又一次 PCIe 写

    发送 32 包 = 32 次 PCIe 写            发送 32 包 = 1 次 PCIe 写

TX Buffer 的三段使用模式(第 852~868 行):

c 复制代码
// ① 分配
tx_buffer[portid] = rte_zmalloc_socket("tx_buffer",
        RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST), 0,
        rte_eth_dev_socket_id(portid));

// ② 初始化
rte_eth_tx_buffer_init(tx_buffer[portid], MAX_PKT_BURST);

// ③ 注册失败回调
rte_eth_tx_buffer_set_err_callback(tx_buffer[portid],
        rte_eth_tx_buffer_count_callback,
        &port_statistics[portid].dropped);

// === 运行时 ===

// ④ 缓冲发送(在主循环的收包循环中调用)
sent = rte_eth_tx_buffer(dst_port, 0, buffer, m);

// ⑤ 定时强制刷新(在 ~100µs 跳帧逻辑中调用)
sent = rte_eth_tx_buffer_flush(portid, 0, buffer);

2.2 lcore 到端口的分配策略

l2fwd 实现了一个灵活的多核端口分配算法:

复制代码
  启用的端口: 0, 1, 2, 3, 4, 5  (6个端口)
  启用的 lcore: 0, 1, 2, 3      (4个核)
  -q 1 (每核负责1个端口)

  分配结果:
  lcore 0: RX port 0 → TX port 1    (0↔1)
  lcore 1: RX port 1 → TX port 0    (1↔0)
  lcore 2: RX port 2 → TX port 3    (2↔3)
  lcore 3: RX port 3 → TX port 2    (3↔2)

  lcore 0,1,2,3 各有活干。端口 4,5 未被分配(因为只有 4 个 lcore 各处理 1 个端口)

  -q 2 (每核负责2个端口):
  lcore 0: RX port 0 → TX 1, RX port 1 → TX 0
  lcore 1: RX port 2 → TX 3, RX port 3 → TX 2
  lcore 2: RX port 4 → TX 5, RX port 5 → TX 4
  lcore 3: 无活干,退出

分配代码逻辑(第 737~765 行):

c 复制代码
rx_lcore_id = 0;
RTE_ETH_FOREACH_DEV(portid) {
    // 跳过未启用端口
    if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)
        continue;

    // 找到下一个有空位的 lcore
    while (rte_lcore_is_enabled(rx_lcore_id) == 0 ||
           lcore_queue_conf[rx_lcore_id].n_rx_port == l2fwd_rx_queue_per_lcore) {
        rx_lcore_id++;                                    // 满了就下一个
    }

    qconf = &lcore_queue_conf[rx_lcore_id];
    qconf->rx_port_list[qconf->n_rx_port] = portid;      // 分配!
    qconf->n_rx_port++;
}

这个算法的特点是:轮询分配 ,保证端口在所有启用的 lcore 之间均匀分布

2.3 MAC 地址重写机制

l2fwd 默认开启 MAC updating:在转发之前重写包的源和目的 MAC 地址

为什么需要 MAC 重写?

复制代码
  ┌──────────┐          ┌──────────┐
  │ 发送端设备 │          │ 接收端设备 │
  │ (真实MAC) │          │ (真实MAC) │
  └──────────┘          └──────────┘
       │                      ▲
       │   原始包              │   经过 l2fwd 转发
       │   dst=接收端MAC       │   dst=02:00:00:00:00:01 (虚拟MAC)
       │   src=发送端MAC       │   src=l2fwd Port1 的 MAC
       ▼                      │
  ┌──────────────────────────────────┐
  │           l2fwd (转发器)          │
  │  Port 0 ← RX...TX → Port 1      │
  └──────────────────────────────────┘

没有 MAC 重写时:包的 src MAC 还是原始发送方→接收方直接把包发给原始发送方回复,绕过了 l2fwd(无法做透明代理/防火墙/负载均衡)。

有 MAC 重写时:src MAC 变为 l2fwd 出口端口的 MAC→回复包自然会回到 l2fwd→形成透明转发链路

目的 MAC 编码规则

复制代码
02:00:00:00:00:XX  (其中 XX = 出口端口号)

这是一个虚拟 MAC 地址(locally administered 位设置了),接收方看到这个地址就知道"这是从 l2fwd 的端口 XX 转发来的"。

2.4 TSC 定时器统计 + 优雅退出

l2fwd 将 timer 示例中学到的 TSC 定时技术信号处理 结合,实现了工业级的定时统计和优雅退出:

复制代码
  SIGINT / SIGTERM
       │
       ▼
  signal_handler()
       │
       ▼
  force_quit = true ────────────────┐
                                    │
  ┌─────────────────────────────────┘
  │
  ▼
  l2fwd_main_loop 中:
  while (!force_quit) {          ← 检测到 true,退出循环
      // ... 收包转发 ...
  }
       │
       ▼
  rte_eal_mp_remote_launch → 所有 lcore 返回
       │
       ▼
  main() 中:
  RTE_LCORE_FOREACH_WORKER {
      rte_eal_wait_lcore()      ← 等待所有 worker 退出
  }
       │
       ▼
  rte_eth_dev_stop() / close()   ← 停止和关闭所有网卡
       │
       ▼
  rte_eal_cleanup()
       │
       ▼
  打印 "Bye..."

与 skeleton 的 for(;;) 死循环相比,优雅退出允许停止网卡、释放资源、打印最终统计,这在生产环境中是必需的。


3. 编译与运行

3.1 编译

bash 复制代码
# 在 DPDK 源码根目录
cd dpdk-22.07

# 方式一:meson + ninja
meson setup build
cd build
meson configure -Dexamples=l2fwd
ninja

# 方式二:独立 Makefile
cd examples/l2fwd
make

3.2 运行前提

  • root 权限 + hugepage 已挂载
  • 至少 2 个 DPDK 兼容网卡端口(建议偶数个),绑定到 DPDK 驱动

3.3 运行

bash 复制代码
# 基本运行:2 端口,4 个 lcore,默认配对 0↔1
sudo ./build/l2fwd -l 0-3 -a 0000:01:00.0 -a 0000:01:00.1 -- -p 0x3

# 期望输出(每 10 秒刷新):
# Port statistics ====================================
# Statistics for port 0 ------------------------------
# Packets sent:                    1234567
# Packets received:                1234567
# Packets dropped:                       0
# ...

3.4 常用参数组合

命令 行为
-p 0x3 -q 1 端口 0,1,每 lcore 1 个端口,默认配对 0↔1
-p 0xf -q 2 端口 0~3,每 lcore 2 个端口,配对 0↔1, 2↔3
-T 5 每 5 秒刷新统计(默认 10 秒)
-T 0 禁用统计打印
-P 所有端口开启混杂模式
--no-mac-updating 原样转发不改 MAC(透明网桥模式)
--portmap="(0,2)(1,3)" 自定义配对:0→2, 2→0, 1→3, 3→1

4. 程序执行流程可视化

复制代码
                              main()
                                │
                                ▼
                       ┌─────────────────┐
                       │  rte_eal_init()  │
                       │  signal() 注册    │
                       │  l2fwd_parse_args│
                       └─────────────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │  端口配对计算     │
                       │  l2fwd_dst_ports │
                       │  [0]=1, [1]=0... │
                       └─────────────────┘
                                │
                                ▼
                    ┌──────────────────────┐
                    │  lcore ←→ 端口分配    │
                    │  lcore_queue_conf[0]: │
                    │    rx_port_list=[0]   │
                    │  lcore_queue_conf[1]: │
                    │    rx_port_list=[1]   │
                    └──────────────────────┘
                                │
                                ▼
                    ┌──────────────────────┐
                    │  创建 mbuf pool       │
                    │  初始化所有端口       │
                    │  创建 TX Buffer       │
                    │  (每端口一个)         │
                    └──────────────────────┘
                                │
                                ▼
             rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, CALL_MAIN)
                                │
                    ┌───────────┴───────────┐
                    ▼                       ▼
        ┌──────────────────┐    ┌──────────────────┐
        │ lcore 0 主循环    │    │ lcore 1 主循环    │
        │                  │    │                  │
        │ while(!quit) {   │    │ while(!quit) {   │
        │   ┌──────────┐   │    │   ┌──────────┐   │
        │   │每~100µs: │   │    │   │每~100µs: │   │
        │   │ flush TX │   │    │   │ flush TX │   │
        │   │ 统计打印 │   │    │   │ 统计打印 │   │
        │   └──────────┘   │    │   └──────────┘   │
        │   ┌──────────┐   │    │   ┌──────────┐   │
        │   │每轮循环: │   │    │   │每轮循环: │   │
        │   │ port 0   │   │    │   │ port 1   │   │
        │   │ rx_burst │   │    │   │ rx_burst │   │
        │   │  ↓       │   │    │   │  ↓       │   │
        │   │ MAC重写  │   │    │   │ MAC重写  │   │
        │   │  ↓       │   │    │   │  ↓       │   │
        │   │tx_buffer │   │    │   │tx_buffer │   │
        │   │  →port1  │   │    │   │  →port0  │   │
        │   └──────────┘   │    │   └──────────┘   │
        │ }                │    │ }                │
        └──────────────────┘    └──────────────────┘
                    │                       │
              Ctrl+C 信号                  Ctrl+C 信号
                    │                       │
                    ▼                       ▼
              force_quit=true          force_quit=true
              退出循环                  退出循环

                    │                       │
                    └───────────┬───────────┘
                                ▼
                    rte_eal_wait_lcore() × N
                                │
                                ▼
                    ┌──────────────────────┐
                    │  rte_eth_dev_stop()  │  ← 停止网卡
                    │  rte_eth_dev_close() │  ← 关闭网卡
                    │  rte_eal_cleanup()   │  ← 清理 EAL
                    └──────────────────────┘
                                │
                                ▼
                          "Bye..."

  lcore 0 主循环细节:
  ═══════════════════════════════════════════════════
   时间 →  (每个竖线是一个 while 循环迭代)
   ═══════════════════════════════════════════════════
  
   迭代 N:      迭代 N+1:      迭代 N+2:      迭代 N+3:
   ┌────────┐  ┌────────┐    ┌────────┐    ┌────────┐
   │读 TSC  │  │读 TSC  │    │读 TSC  │    │读 TSC  │
   │diff<   │  │diff<   │    │diff>   │    │diff<   │
   │100µs   │  │100µs   │    │100µs!  │    │100µs   │
   │→跳过   │  │→跳过   │    │→执行! │    │→跳过   │
   │        │  │        │    │        │    │        │
   │port0   │  │port0   │    │TX flush│    │port0   │
   │rx_burst│  │rx_burst│    │→port1  │    │rx_burst│
   │→8个包  │  │→0个包  │    │统计打印│    │→12个包 │
   │tx_buf  │  │continue│    │        │    │tx_buf  │
   │→缓冲8个│  │        │    │port0   │    │→缓冲12 │
   └────────┘  └────────┘    │rx_burst│    └────────┘
                              │→16个包 │
                              │tx_buf  │
                              │→缓冲16 │
                              └────────┘
                              ↑ 此时 TX Buffer 中攒了 8+16=24 个包
                              ↑ flush 一次性发出
  ═══════════════════════════════════════════════════

5. 与其他例程的关系

l2fwd 是 skeleton 的工业级增强版,也整合了 timer 的定时器技术:

维度 skeleton l2fwd timer
端口初始化 固定 1 RX + 1 TX 固定 1 RX + 1 TX
多 lcore ❌ 单核 ✅ 多核(端口-lcore 分配)
TX 方式 直接 rte_eth_tx_burst rte_eth_tx_buffer + 定时 flush
MAC 重写 ✅(可选)
端口配对 port ^ 1 硬编码 默认交替 + --portmap 自定义
统计打印 ✅ TSC 定时器 + ANSI 刷新仪表盘 ✅ (TSC 定时器)
信号处理 ❌ Ctrl+C 强杀 ✅ SIGINT/SIGTERM 优雅退出
TX 失败处理 手动 pktmbuf_free error callback 自动统计 drop
预取优化 rte_prefetch0
mbuf 池计算 固定 8191×n 按描述符+burst+cache 动态计算

递进关系

复制代码
skeleton ──────→ l2fwd ──────→ l3fwd
(骨架转发)      (二层转发)     (三层转发/查表)
    │                │
    │                ├─ TX Buffer
    │                ├─ MAC 重写
    │                ├─ 多核端口分配
    │                ├─ 动态 mbuf 计算
    │                └─ 信号处理+优雅退出
    │
  timer ──────────→ (TSC 定时统计)

6. 常见面试问题

Q1: l2fwd 的 TX Buffer 和直接 rte_eth_tx_burst 的区别?为什么要用 TX Buffer?

:TX Buffer 是一个软件缓冲区,它将零散的 mbuf 先缓存起来,攒到一定的 batch size 或超时后再一次性 flush 到硬件 TX 描述符环。好处是:(1) 减少 PCIe 写操作次数→提高吞吐;(2) 自动处理发送失败(通过 error callback 计数)。代价是增加了 100µs 级别的延迟。

Q2: l2fwd 如何将端口分配给不同的 lcore?

:通过 lcore_queue_conf 数组,main 函数在初始化时遍历所有启用的网卡端口,轮询分配 给启用的 lcore。每个 lcore 可以处理 l2fwd_rx_queue_per_lcore-q 参数)个端口。如果一个 lcore 没有被分配任何端口(如端口数 < lcore 数),它会检测到 n_rx_port == 0 并直接退出。

Q3: 为什么只在 main lcore 上打印统计?

:(1) 避免多个核同时 printf 导致输出交织乱码;(2) 减少对转发性能的影响(printf 是系统调用,开销大)。main lcore 也参与转发,它只是"兼职"打印统计。

Q4: rte_prefetch0() 在这里有什么作用?

:在处理 mbuf 时,rte_prefetch0(rte_pktmbuf_mtod(m, void *)) 将当前包的以太网头部数据提前加载到 L1 Cache。由于 CPU 访问内存的延迟远高于 Cache,预取下一个包的数据可以让 CPU 在处理当前包时"后台"加载下一个------这就是软件流水线的思想。对于小包场景,这个优化可以显著提升吞吐。

Q5: MAC 重写的作用是什么?为什么要改成 02:00:00:00:00:XX

:MAC 重写实现了透明转发 :src MAC 替换为 l2fwd 出口端口的真实 MAC→数据包的来源对接收方"隐藏"了,接收方看到的发包者是 l2fwd。dst MAC 改为 02:00:00:00:00:XX(XX=端口号)是一个虚拟 MAC(locally administered bit 置 1),便于调试和追踪数据包经过了哪个端口。如果 --no-mac-updating,l2fwd 就像一个普通网桥。

Q6: l2fwd 的优雅退出流程是怎样的?

:SIGINT → signal_handler 设置 force_quit = true → 各 lcore 的 l2fwd_main_loop 检测到 force_quit 退出循环 → main 中 rte_eal_wait_lcore 等待所有核退出 → rte_eth_dev_stop/close 停止并关闭网卡 → rte_eal_cleanup 清理。这个顺序确保网卡在进程退出前被正确关闭。

Q7: mbuf pool 的大小是如何计算的?为什么不用固定值?

nb_mbufs = RTE_MAX(nb_ports * (nb_rxd + nb_txd + MAX_PKT_BURST) + nb_lcores * MEMPOOL_CACHE_SIZE, 8192U)。覆盖了 RX 描述符环中的 mbuf + TX 描述符环中的 mbuf + burst 处理中的 mbuf + 各 lcore 的缓存。这种动态计算使得池大小随端口数和 lcore 数自适应,不浪费 hugepage 内存。工业级 DPDK 应用都采用类似方式。


7. 延伸阅读


8. 本示例涉及的 API 总结

8.1 API 速查表

# API 类型 所属头文件 功能说明
1 rte_eal_init(argc, argv) 函数 <rte_eal.h> EAL 初始化。
2 rte_eal_cleanup() 函数 <rte_eal.h> 清理 EAL 资源。
3 rte_exit(code, fmt, ...) 函数 <rte_eal.h> 格式化打印后退出。
4 rte_lcore_id() 函数 <rte_lcore.h> 获取当前 lcore ID。
5 rte_lcore_is_enabled(id) 函数 <rte_lcore.h> 检查某 lcore 是否被启用(通过 -l-c 参数)。
6 rte_get_main_lcore() 函数 <rte_lcore.h> 获取 main lcore ID。
7 rte_eal_mp_remote_launch(f, arg, CALL_MAIN) 函数 <rte_launch.h> 在所有 lcore(包括 main)上启动函数。CALL_MAIN 标志使 main lcore 也参与执行。
8 rte_eal_wait_lcore(id) 函数 <rte_launch.h> 等待单个 lcore 上的任务完成并返回。用于逐个等待优雅退出。
9 rte_get_timer_hz() 函数 <rte_cycles.h> 获取 TSC 频率。用于将秒转换为 TSC cycles。
10 rte_rdtsc() 函数 <rte_cycles.h> 直接读取 CPU TSC 寄存器(一条 RDTSC 指令)。比 rte_get_timer_cycles() 更底层。
11 rte_eth_dev_count_avail() 函数 <rte_ethdev.h> 获取可用以太网端口总数。
12 rte_eth_dev_is_valid_port(port) 函数 <rte_ethdev.h> 检查端口 ID 有效性。
13 rte_eth_dev_info_get(port, &info) 函数 <rte_ethdev.h> 获取网卡硬件能力信息。
14 rte_eth_dev_configure(port, rx_n, tx_n, &conf) 函数 <rte_ethdev.h> 配置网卡端口。l2fwd 中每个端口只配 1 RX + 1 TX。
15 rte_eth_dev_adjust_nb_rx_tx_desc(port, &rxd, &txd) 函数 <rte_ethdev.h> 校准描述符环大小。
16 rte_eth_rx_queue_setup(port, q, nb_rxd, socket, &rxq_conf, pool) 函数 <rte_ethdev.h> 分配并初始化 RX 队列。l2fwd 增加了 &rxq_conf 用于 offload 配置。
17 rte_eth_tx_queue_setup(port, q, nb_txd, socket, &txq_conf) 函数 <rte_ethdev.h> 分配并初始化 TX 队列。
18 rte_eth_dev_start(port) 函数 <rte_ethdev.h> 启动网卡端口。
19 rte_eth_dev_stop(port) 函数 <rte_ethdev.h> 停止网卡端口(优雅退出时调用)。
20 rte_eth_dev_close(port) 函数 <rte_ethdev.h> 关闭网卡端口,释放硬件资源。
21 rte_eth_promiscuous_enable(port) 函数 <rte_ethdev.h> 开启混杂模式(仅当 -P 参数指定时)。
22 rte_eth_macaddr_get(port, &addr) 函数 <rte_ethdev.h> 获取网卡 MAC 地址。
23 rte_eth_link_get_nowait(port, &link) 函数 <rte_ethdev.h> 非阻塞获取端口链路状态。
24 rte_eth_rx_burst(port, queue, bufs, nb) 函数 <rte_ethdev.h> 批量收包。
25 rte_eth_tx_buffer(port, queue, buffer, m) 函数 <rte_ethdev.h> TX Buffer 缓冲发送。不直接发,先写入软件缓冲区。返回已 flush 的数量。
26 rte_eth_tx_buffer_flush(port, queue, buffer) 函数 <rte_ethdev.h> 强制刷新 TX Buffer。将缓冲区中所有待发送的包一次性发送。用于定时 flush。
27 rte_eth_tx_buffer_init(buffer, size) 函数 <rte_ethdev.h> 初始化 TX Buffer(设置 max burst size)。
28 rte_eth_tx_buffer_set_err_callback(buffer, cb, userdata) 函数 <rte_ethdev.h> 设置 TX Buffer 发送失败的回调函数。
29 rte_eth_tx_buffer_count_callback(unsent, count, userdata) 函数 <rte_ethdev.h> 内置的失败计数回调 :将未发送的 mbuf 全部 free,并累加 *userdata 计数。drop 统计就是用它。
30 rte_eth_dev_set_ptypes(port, RTE_PTYPE_UNKNOWN, ...) 函数 <rte_ethdev.h> 禁用网卡硬件 Ptype 解析(关闭硬件解析,由软件自己分析包类型)。
31 rte_pktmbuf_pool_create(name, n, cache, priv, size, socket) 函数 <rte_mbuf.h> 创建 mbuf 内存池。
32 rte_pktmbuf_mtod(m, type) <rte_mbuf.h> 核心宏 :将 mbuf 的数据区指针转换为指定类型(如 struct rte_ether_hdr *)。
33 rte_pktmbuf_free(m) 函数 <rte_mbuf.h> 释放单个 mbuf 回 mempool(用于 TX 失败回调)。
34 rte_ether_addr_copy(&src, &dst) 函数 <rte_ether.h> 复制 MAC 地址。用于 MAC 重写时设置 src MAC。
35 rte_zmalloc_socket(name, size, align, socket) 函数 <rte_malloc.h> 在指定 NUMA node 上分配清零内存。用于创建 TX Buffer。
36 rte_prefetch0(addr) <rte_prefetch.h> 预取数据到 L1 Cache。处理 mbuf 时提前加载下一个包的以太网头。
37 RTE_ETH_FOREACH_DEV(port) <rte_ethdev.h> 遍历所有已探测的网卡端口。
38 unlikely(expr) <rte_branch_prediction.h> 分支预测提示:该分支不太可能发生。
39 RTE_LOG(level, type, fmt, ...) <rte_log.h> DPDK 分级日志。l2fwd 用 RTE_LOGTYPE_L2FWD 类型。
40 RTE_ETH_TX_BUFFER_SIZE(max_burst) <rte_ethdev.h> 计算 TX Buffer 所需的内存大小。

8.2 按调用顺序的调用关系图

复制代码
main()
  │
  ├─ [1]  rte_eal_init(argc, argv)          ← EAL 初始化
  ├─ [3]  l2fwd_parse_args(argc, argv)       ← 解析 -p/-q/-T/--portmap 等
  ├─ [9]  rte_get_timer_hz()                 ← TSC 频率 → timer_period 转 cycles
  │
  ├─ [11] rte_eth_dev_count_avail()           ← 获取端口数
  ├─ [37] RTE_ETH_FOREACH_DEV →              ← lcore-端口分配
  │       ┌─ [5] rte_lcore_is_enabled()
  │       └─ 填充 lcore_queue_conf[]
  │
  ├─ [31] rte_pktmbuf_pool_create(...)       ← 动态计算池大小
  │
  ├─ [37] RTE_ETH_FOREACH_DEV →              ← 端口初始化
  │   └─ port_init (内联)
  │       ├─ [13] rte_eth_dev_info_get()
  │       ├─ [14] rte_eth_dev_configure()
  │       ├─ [15] rte_eth_dev_adjust_nb_rx_tx_desc()
  │       ├─ [22] rte_eth_macaddr_get()
  │       ├─ [16] rte_eth_rx_queue_setup()
  │       ├─ [17] rte_eth_tx_queue_setup()
  │       ├─ [35] rte_zmalloc_socket()       ← 分配 TX Buffer
  │       ├─ [27] rte_eth_tx_buffer_init()   ← 初始化 TX Buffer
  │       ├─ [28] rte_eth_tx_buffer_set_err_callback() ← 设置失败回调
  │       ├─ [30] rte_eth_dev_set_ptypes()   ← 关闭硬件 Ptype
  │       ├─ [18] rte_eth_dev_start()
  │       └─ [21] rte_eth_promiscuous_enable() ← (可选)
  │
  ├─ [7]  rte_eal_mp_remote_launch(..., CALL_MAIN)
  │   └─ l2fwd_main_loop() × N              ← 各核循环
  │       │
  │       └─ while (!force_quit)
  │           ├─ [10] rte_rdtsc()
  │           ├─ if (diff_tsc > drain_tsc)   ← 每 ~100µs
  │           │   ├─ [26] rte_eth_tx_buffer_flush()
  │           │   └─ [6] rte_get_main_lcore() → print_stats()
  │           │
  │           └─ for each rx_port
  │               ├─ [24] rte_eth_rx_burst()
  │               ├─ [32] rte_pktmbuf_mtod()
  │               ├─ [36] rte_prefetch0()     ← 预取优化
  │               ├─ [34] rte_ether_addr_copy() ← MAC 重写
  │               └─ [25] rte_eth_tx_buffer()  ← 缓冲发送
  │
  ├─ [8]  rte_eal_wait_lcore() × N           ← 等待所有核退出
  │
  ├─ [19] rte_eth_dev_stop()                 ← 停止网卡
  ├─ [20] rte_eth_dev_close()                ← 关闭网卡
  └─ [2]  rte_eal_cleanup()                  ← 清理 EAL

  signal_handler (SIGINT/SIGTERM)
  └─ force_quit = true                       ← 触发优雅退出

8.3 API 分类

分类 API 用途场景
生命周期 rte_eal_initrte_eal_cleanup 标准 DPDK 程序骨架
多核调度 rte_eal_mp_remote_launch + rte_eal_wait_lcore + rte_lcore_is_enabled + rte_get_main_lcore 广播主循环到所有核 + 逐个等待退出
端口配置 rte_eth_dev_* 系列(info_get / configure / adjust / rx/tx_queue_setup / start / stop / close / promiscuous_enable / macaddr_get / link_get_nowait / set_ptypes) 标准端口初始化的 9 步流程 + 优雅退出清理
数据包收发 rte_eth_rx_burst + rte_eth_tx_buffer + rte_eth_tx_buffer_flush 批量收包 + 缓冲发送 + 定时刷新
TX Buffer 管理 rte_eth_tx_buffer_init + rte_eth_tx_buffer_set_err_callback + rte_eth_tx_buffer_count_callback TX Buffer 初始化、失败回调、drop 统计
内存管理 rte_pktmbuf_pool_create + rte_pktmbuf_mtod + rte_pktmbuf_free + rte_zmalloc_socket mbuf 池创建 + 数据指针获取 + 释放 + TX Buffer 内存分配
MAC 操作 rte_ether_addr_copy MAC 地址复制(重写 src MAC)
性能优化 rte_prefetch0 + unlikely 预取 + 分支预测提示
定时/时间 rte_get_timer_hz + rte_rdtsc TSC 频率获取 + 直接读 TSC
日志 RTE_LOG DPDK 分级日志打印

下一步学习建议 :l2fwd 是 DPDK 网络应用的标准范式。理解后建议学习 l3fwd(三层转发,在 l2fwd 基础上加入 LPM 路由查表和哈希流分类),理解如何在二层转发骨架上构建三层 IP 转发。

相关推荐
努力努力再努力FFF2 小时前
大学四年AI能力规划:从入门学习到简历表达
人工智能·学习
Litluecat2 小时前
配合多角色提示语3,学习AI漫剧(刚开始学)
人工智能·学习·ai·提示词·短剧·漫剧
三品吉他手会点灯2 小时前
STM32F103 学习笔记-24-I2C-读写EEPROM(第1节)-I2C物理层介绍
笔记·stm32·学习
MartinYeung52 小时前
[论文学习]大型语言模型中个人可识别资讯(PII)的机器遗忘技术:UnlearnPII 基准与 PERMU_tok 方法的深度分析
人工智能·学习·语言模型
fanged2 小时前
Linux内核学习21--V4L2学习3(应用)(TODO)
学习
GHL2842710904 小时前
PowerShell快捷键学习
学习
半导体守望者5 小时前
AE电源闭环控制——反应溅射的集成解决方案
经验分享·学习·机器人·自动化·制造
小饕5 小时前
RAG学习之【向量数据库】Milvus 从入门到精通:索引、检索、混合搜索一篇打通(RAG 必备)
数据库·人工智能·学习·milvus
xianrenli386 小时前
MSAI:第四周练习:思维链 (Chain-of-Thought) 提示与参数调优
学习·msai