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 |
本示例最核心的数据结构 。将每个启用的网卡端口分配给特定的 lcore :lcore_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 行)
统计打印 print_stats(第 119~164 行)
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 = ð->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
ð->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_MAIN :rte_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. 延伸阅读
- DPDK 官方文档 - L2 Forwarding Sample App
- DPDK Programmer's Guide - Ethernet Device API
- DPDK Programmer's Guide - Mbuf Library
- DPDK API Reference - rte_eth_tx_buffer
- DPDK API Reference - rte_prefetch
- 同目录下的
skeleton/(l2fwd 的基础骨架版) - 同目录下的
timer/(TSC 定时器基础) - 同目录下的
l3fwd/(三层转发,在 l2fwd 基础上增加 LPM 查表路由)
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_init → rte_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 转发。