【DPDK】DPDK 25.11 I2fwd源码学习分析
DPDK 25.11 I2fwd源码学习分析
功能简介
将一个物理网口收到的包立刻从另一个物理网口发出去(通常用于测试网卡吞吐量)
经典的DPDK 二层转发(Layer 2 Forwarding)示例程序,展示了DPDK应用的基础骨架:轮询模式驱动(PMD)、无中断收发包、零拷贝。
l2fwd 演示了 DPDK 的 Run-to-Completion 模型:
一个 CPU 核心拿着网卡队列,死循环 -> 抓一把包 -> 简单处理 -> 扔到另一个网卡队列 -> 继续抓包。
相关问题
-
MAC地址改写(关键点):交换机转发包是不改MAC的,但这里是网卡到网卡。为了让对端设备(或另一台电脑)能识别并接收,它会把源MAC改成发出端口自己的MAC,把目的MAC改成一个伪造的组播/单播地址(
02:00:00:00:00:TX_PORT_ID)。-
源MAC不改的话,网卡硬件可能拒发,回包也无法路由回来
-
源MAC修改后的数据包收发流程
-
服务器B 发包(源MAC =
BB:BB:BB:BB:BB:BB)→ 到达 Port 0 -
l2fwd 转发时,把源MAC改成 Port 1 的MAC(
11:11:11:11:11:01)|普通的不会改,还是BB) -
从port1发出后,对端设备收到包,看到源MAC =
11:11:11:11:11:01|(BB) -
对端设备回复时:
“我要回复给
11:11:11:11:11:01,这个地址就是我眼前这根网线的对端啊!” -
回包顺着原路发回来 → 到达 Port 1
-
l2fwd 再次转发,从 Port 0 出去,回到服务器B
至此 双向通路建立成功。
-
-
目的MAC修改流程
- 用 l2fwd 做"回环测试"时(Port 0 和 Port 1 直连,没有外部设备)
- 抓包教学演示
-
-
为什么不做下面这个流程【到处发(洪泛 Flood)→ 看到回包 → 记录 MAC 和端口的对应关系 → 填表 → 下次直接转发】
- 洪泛学习有代价,I2fwd是DPDK基础性能测试基准,追求线速;
- I2fwd经典用法之一是串接在网络中间做透明监控或加解密。通过强制修改源MAC,使两边被物理隔离,不会产生直接的ARP对话,但逻辑上是通的
-
收包和发包没有锁
- Per-Core Queue:代码中给每个逻辑核分配了独立的
lcore_queue_conf。 - 逻辑核 Core 1 只操作端口0的收队列0 和 端口1的发队列0。
- 逻辑核 Core 2 只操作端口1的收队列0 和 端口0的发队列0。
- 结果:两个核心永远不会争抢同一个队列的锁。这就是 DPDK 的 无锁数据通道。
- Per-Core Queue:代码中给每个逻辑核分配了独立的
命令行解析
* 参数输入 ./l2fwd -c 0x3 -n 4 -- -p 3 -q 1
* -c 为十六进制的分配的逻辑内核数量
* -n 为十进制的内存通道数量,EAL参数和程序参数用--分开
* -q 为分配给每个核心的收发队列数量(端口数量)
* -p为十六进制的分配的端口数
* -t 为可选默认10s打印时间间隔参数

代码解读
1. main函数部分
-
第一阶段:DPDK起手
ret = rte_eal_init(argc, argv);- 解析 EAL 参数(比如
-l 0-3指定用哪些 CPU 核,-a 00:04.0指定用哪个 PCIe 网卡),在DPDK中--用来分隔EAL参数和应用参数。 - 初始化大页内存:从系统预留的大页中分配内存,建立映射表
- 设置 CPU 亲缘性:把当前主线程绑定到某个核上
- 扫描 PCI 总线:找到我们指定的网卡设备
argc -= ret;//剩余参数个数 argv += ret;//移动指针到后续还未解析的参数 - 解析 EAL 参数(比如
-
第二阶段:信号处理与应用参数解析
force_quit = false; signal(SIGINT, signal_handler); // Ctrl+C signal(SIGTERM, signal_handler); // kill 命令 /* parse application arguments (after the EAL ones) */ ret = l2fwd_parse_args(argc, argv); if (ret < 0) rte_exit(EXIT_FAILURE, "Invalid L2FWD arguments\n");- 信号注册:当用户按 Ctrl+C 时,
signal_handler会把force_quit设为true。各个核上的死循环看到这个标志就会优雅退出。 l2fwd_parse_args进行参数解析,支持短参数(-p 0x3)与长参数(--mac-updating),其使用getopt_long- 是 Linux 命令行参数解析器,自动把用户在命令行输入的
-p 0x3、--help、--file=test.txt这类东西,转换成C 语言代码里能直接用的变量和值。
- 是 Linux 命令行参数解析器,自动把用户在命令行输入的
- 信号注册:当用户按 Ctrl+C 时,
-
第三阶段:参数校验和预处理阶段
- 确保用户给的参数是合法且可执行
printf("MAC updating %s\n", mac_updating ? "enabled" : "disabled"); /* convert to number of cycles */ timer_period *= rte_get_timer_hz(); //把秒转换成CPU时钟周期数- 为什么把秒转换时钟周期,转发循环里用的是
rte_rdtsc()读取 CPU 时间戳(从 CPU 启动到现在的时钟周期数),这样可以直接比较,避免每次都调用耗时的时间函数。、
nb_ports = rte_eth_dev_count_avail(); if (nb_ports == 0) rte_exit(EXIT_FAILURE, "No Ethernet ports - bye\n");- 检查可用网口数量,查询DPDK接管了几个网口,如果没有则报错退出。
if (port_pair_params != NULL) { if (check_port_pair_config() < 0) rte_exit(EXIT_FAILURE, "Invalid port pair config\n"); }- 检查端口配对配置(新版l2fwd功能):如果用 --portmap 参数手动指定了端口配对关系(比如 “(0,2),(1,3)”),这里会检查配置是否合法。
/* check port mask to possible port mask */ if (l2fwd_enabled_port_mask & ~((1 << nb_ports) - 1)) rte_exit(EXIT_FAILURE, "Invalid portmask; possible (0x%x)\n", (1 << nb_ports) - 1); /* Initialization of the driver. 8< */- 检查端口掩码是否越界,确保用户 -p 参数指定的端口号不超过实际可用端口范围
-
第四阶段:确定端口配对关系
/* reset l2fwd_dst_ports */ for (portid = 0; portid < RTE_MAX_ETHPORTS; portid++) l2fwd_dst_ports[portid] = 0; last_port = 0;- 清空目的端口表,索引是收包端口号,值是发包端口号
RTE_MAX_ETHPORTS是DPDK框架级的宏定义,定义在DPDK的头文件里,表示其最多能支持多少个以太网端口
/* populate destination port details */ if (port_pair_params != NULL) { uint16_t idx, p; for (idx = 0; idx < (nb_port_pair_params << 1); idx++) { p = idx & 1; portid = port_pair_params[idx >> 1].port[p]; l2fwd_dst_ports[portid] = port_pair_params[idx >> 1].port[p ^ 1]; } }- 填充目的端口表的第一种情况:用户手动指定了端口配对
port_pair_params通过用户使用--portmap参数手动指定配对 eg../l2fwd... --portmap="(0, 2), (1, 3)"nb_port_pair_params通过端口配对解析函数l2fwd_parse_port_pair_config获得- 循环迭代进行双向端口设置
idx & 1判断idx是偶数还是奇数,& 1只保留二进制的最低位idx >> 1把idx除以2(整数除法),没两次迭代映射到同一对端口nb_port_pair_params << 1把nb_port_pair_params乘2,每对端口需要两次迭代来进行双向设置p ^ 1异或运算0 ^ 1 = 1,1 ^ 1 = 0,把 0 变成 1,把 1 变成 0(翻转)
else { RTE_ETH_FOREACH_DEV(portid) { /* skip ports that are not enabled */ if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)//跳过未启用端口 continue; if (nb_ports_in_mask % 2) { l2fwd_dst_ports[portid] = last_port; l2fwd_dst_ports[last_port] = portid; } else { last_port = portid; } nb_ports_in_mask++; } if (nb_ports_in_mask % 2) { printf("Notice: odd number of ports in portmask.\n"); l2fwd_dst_ports[last_port] = last_port; } } /* >8 End of initialization of the driver. */- 填充目的端口表的第二种情况:用户没有手动指定,使用默认自动配对
- 如果总数是奇数个端口,最后一个端口自己发给自己,
- 如果总数是偶数个端口,每两个端口双向链接。
RTE_ETH_FOREACH_DEV是DPDK框架自带的宏,遍历所有被DPDK接管的、可用的网口,定义在头文件<rte_ethdev.h>下,RTE_ETH_FOREACH_DEV 遍历的是被 DPDK 接管的有效端口,所以:- 如果你的系统有 4 个物理网口,但只通过 -a 参数绑定了端口 0 和端口 2,那么 portid 的值会是:0 → 2 → 结束。
- 如果启动命令里没有用 -a 指定网口,那么 DPDK 会接管所有它识别到的、且没有被内核驱动占用的网口。
l2fwd_enabled_port_mask是用户-p参数指定的掩码,如果当前端口不在掩码里就跳过eg.-p 0x5二进制0101启用端口0、2nb_ports_in_mask初始为0,用来统计用户启用了几个端口last_port用来记住上一个遍历的端口号,初始为0
-
第五阶段:分配逻辑核与端口
-
建立多对多映射:哪些 CPU 核负责从哪些网口收包
-
背景:代码中数据结构定义
#define MAX_RX_QUEUE_PER_LCORE 16 struct __rte_cache_aligned lcore_queue_conf { unsigned n_rx_port;//该核负责几个端口 unsigned rx_port_list[MAX_RX_QUEUE_PER_LCORE]; //具体是哪几个端口 }; struct lcore_queue_conf lcore_queue_conf[RTE_MAX_LCORE];RTE_MAX_LCOREDPDK 框架级的宏定义,表示 DPDK 最多能支持多少个逻辑核 lcore。lcore_queue_conf索引是lcore_id,每个元素记录了这个lcore负责从哪些端口收包
rx_lcore_id = 0; qconf = NULL;rx_lcore_id:当前正在分配的逻辑核编号,从 0 开始qconf:指向当前逻辑核的配置结构体,初始为空
/* Initialize the port/queue configuration of each logical core */ RTE_ETH_FOREACH_DEV(portid) { /* skip ports that are not enabled */ if ((l2fwd_enabled_port_mask & (1 << portid)) == 0) continue; /* get the lcore_id for this port */ 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++; if (rx_lcore_id >= RTE_MAX_LCORE) rte_exit(EXIT_FAILURE, "Not enough cores\n"); } if (qconf != &lcore_queue_conf[rx_lcore_id]) { /* Assigned a new logical core in the loop above. */ qconf = &lcore_queue_conf[rx_lcore_id]; nb_lcores++; } qconf->rx_port_list[qconf->n_rx_port] = portid; qconf->n_rx_port++; printf("Lcore %u: RX port %u TX port %u\n", rx_lcore_id, portid, l2fwd_dst_ports[portid]); }l2fwd_rx_queue_per_lcore全局变量,表示每个lcore最多负责几个收包端口。nb_lcores在main函数内定义的局部变量,统计时机分配了收包任务的lcore数量。
-
-
第六阶段:创建内存池(Mempool)
在传统 Linux 网络栈中,每收到一个包,内核要
kmalloc一块内存,用完再kfree。频繁的分配释放会:- 导致内存碎片
- 锁竞争严重
- Cache 污染
DPDK 的做法是:程序启动时,一次性申请一大块内存,切好,分给不同的池子。
nb_mbufs = RTE_MAX(nb_ports * (nb_rxd + nb_txd + MAX_PKT_BURST + nb_lcores * MEMPOOL_CACHE_SIZE), 8192U); /* Create the mbuf pool. 8< */ l2fwd_pktmbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", nb_mbufs, MEMPOOL_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()); if (l2fwd_pktmbuf_pool == NULL) rte_exit(EXIT_FAILURE, "Cannot init mbuf pool\n"); /* >8 End of create the mbuf pool. */-
RTE_MAX是DPDK内部的通用工具宏,取两者中较大值,头文件<rte_common.h>变量 来源 含义 示例值 nb_portsrte_eth_dev_count_avail()返回DPDK 实际接管的网口数量 4 nb_rxd文件开头定义 static uint16_t nb_rxd = 1024每个收队列的描述符数量 1024 nb_txd文件开头定义 static uint16_t nb_txd = 1024每个发队列的描述符数量 1024 MAX_PKT_BURST文件开头 #define MAX_PKT_BURST 32一次最多收/发多少个包(突发收发包) 32 nb_lcores上一阶段统计得到 实际参与转发的 lcore 数量 2 MEMPOOL_CACHE_SIZE文件开头 #define MEMPOOL_CACHE_SIZE 256每个 lcore 的 mbuf 缓存大小 256 8192U硬编码常量 最小 mbuf 数量,保证内存池不会太小 8192 nb_lcores * MEMPOOL_CACHE_SIZE:每个 lcore 有自己的本地缓存,避免频繁访问共享内存池* nb_ports:每个端口都需要这么多
-
rte_pktmbuf_pool_create在大页内存中创建mbuf内存池参数 值 含义 "mbuf_pool"字符串 内存池的名字,调试时用 nb_mbufs10368 池子里有多少个 mbuf MEMPOOL_CACHE_SIZE256 每个 lcore 的本地缓存大小 00 私有数据大小,不需要就是 0 RTE_MBUF_DEFAULT_BUF_SIZE宏(通常 2048 + 128)定义在 rte_mbuh.h每个 mbuf 的数据区大小 rte_socket_id()函数返回值 当前cpu核心所在的 NUMA 节点编号 -
l2fwd_pktmbuf_pool是一个指向struct rte_mempool的指针。后续网卡收包时,会从这个池子里取 mbuf 来装数据。 -
为什么要指定 NUMA 节点?
- 多路服务器上,不同 CPU 访问不同内存条的延迟不同
- 把内存池分配在本地 NUMA 节点上,访问速度最快
-
nb_mbufs计算公式:网卡描述符数量(1024收+1024发)+ 突发缓存(32)+ 每核缓存(256)
-
第七阶段:初始化每个网口(⭐)
/* Initialise each port */ RTE_ETH_FOREACH_DEV(portid) { struct rte_eth_rxconf rxq_conf; struct rte_eth_txconf txq_conf; struct rte_eth_conf local_port_conf = port_conf; struct rte_eth_dev_info dev_info; ... }分成多段来看:
-
首先第一段,跳过未启用的端口,同上阶段的代码,并统计实际可用的端口数量
nb_ports_available。 -
打印初始化信息并获取网卡能力
/* init port */ printf("Initializing port %u... ", portid); fflush(stdout);- 强制刷新输出缓冲区,让文字立刻显示在屏幕上
ret = rte_eth_dev_info_get(portid, &dev_info); if (ret != 0) rte_exit(EXIT_FAILURE, "Error during getting device (port %u) info: %s\n", portid, strerror(-ret));rte_eth_dev_info_get用于查询指定网口的硬件能力和配置信息(填充在dev_info中),声明在头文件<rte_ethdev.h>下,成功返回0;- dev_info是一个信息结构体,包含了网卡的各种能力,比如:驱动名称;最大收发队列数;支持的 offload 功能;速度能力(10G/25G/100G)
- offload功能指是指把原本需要 CPU 软件处理 的网络包操作,交给 网卡硬件 去完成,从而释放 CPU 资源
if (dev_info.tx_offload_capa & RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE) local_port_conf.txmode.offloads |= RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE;-
开启MBUF_FAST_FREE功能
-
dev_info.tx_offload_capa网卡硬件支持的发送 offload 能力(位掩码); -
RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE一个标志位,表示"快速释放 mbuf"功能,通过&运算检查该网卡硬件是否支持该功能,网卡硬件自动完成buf 的释放,不经过软件。减少 CPU 开销,提高小包转发性能。 -
local_port_conf.txmode.offloads要配置给网卡的发送 offload 设置,local_port_conf等于文件开头定义的默认端口配置|=位或赋值,表示把这个标志位加到配置里
-
-
配置网口:1个收队列,1个发队列
/* Configure the number of queues for a port. */ ret = rte_eth_dev_configure(portid, 1, 1, &local_port_conf); if (ret < 0) rte_exit(EXIT_FAILURE, "Cannot configure device: err=%d, port=%u\n", ret, portid); /* >8 End of configuration of the number of queues for a port. */
rte_eth_dev_configure声明在DPDK<rte_ethdev.h>头文件下,用于配置以太网设备。- l2fwd 是一个简单转发程序,每个端口只用一个收队列和一个发队列。
-
调整描述符数量:因为不同网卡对描述符的数量又不同的限制
ret = rte_eth_dev_adjust_nb_rx_tx_desc(portid, &nb_rxd, &nb_txd); if (ret < 0) rte_exit(EXIT_FAILURE, "Cannot adjust number of descriptors: err=%d, port=%u\n", ret, portid);rte_eth_dev_adjust_nb_rx_tx_desc声明在DPDK<rte_ethdev.h>头文件下,用于根据网卡硬件限制,调整描述符数量到合法范围。nb_rxd,nb_txd使文件开头定义的全局静态变量(1024),分别表示每个收队列的描述符数量和每个发队列的描述符数量。
-
获取并存放网口MAC地址:在转发时,l2fwd 会修改包的源MAC地址,改成发出端口自己的MAC地址。
ret = rte_eth_macaddr_get(portid, &l2fwd_ports_eth_addr[portid]); if (ret < 0) rte_exit(EXIT_FAILURE, "Cannot get MAC address: err=%d, port=%u\n", ret, portid);rte_eth_macaddr_get声明在DPDK<rte_ethdev.h>头文件下,用于读取网卡的MAC地址,并把MAC地址存到l2fwd_ports_eth_addr[portid]里l2fwd_ports_eth_addr是文件开头定义的全局数组,保存每个端口的MAC地址,后面转发时会用来改写包的源MAC。
-
设置网口的收队列
/* init one RX queue */ fflush(stdout);//刷新输出缓冲区 rxq_conf = dev_info.default_rxconf; rxq_conf.offloads = local_port_conf.rxmode.offloads; /* RX queue setup. 8< */ ret = rte_eth_rx_queue_setup(portid, 0, nb_rxd, rte_eth_dev_socket_id(portid), &rxq_conf, l2fwd_pktmbuf_pool); if (ret < 0) rte_exit(EXIT_FAILURE, "rte_eth_rx_queue_setup:err=%d, port=%u\n", ret, portid); /* >8 End of RX queue setup. */rte_eth_rx_queue_setup声明在DPDK<rte_ethdev.h>头文件下,用于分配和设置一个收队列,”0“ 是队列编号,l2fwd 只用 1 个队列所以是 0。rte_eth_dev_socket_id声明在DPDK<rte_ethdev.h>头文件下,用于得到网卡所在NUMA节点,指的是网卡这个 PCIe 设备在物理上连接到哪个 CPU,写的是哪个CPU 的内存。l2fwd_pktmbuf_pool,网卡需要知道收包时从哪个内存池取 mbuf 来装数据。
-
设置网口的发队列
/* Init one TX queue on each port. 8< */ fflush(stdout); txq_conf = dev_info.default_txconf; txq_conf.offloads = local_port_conf.txmode.offloads; ret = rte_eth_tx_queue_setup(portid, 0, nb_txd, rte_eth_dev_socket_id(portid), &txq_conf); if (ret < 0) rte_exit(EXIT_FAILURE, "rte_eth_tx_queue_setup:err=%d, port=%u\n", ret, portid); /* >8 End of init one TX queue on each port. */rte_eth_tx_queue_setup,声明在DPDK<rte_ethdev.h>头文件下,用于分配和设置一个发队列。- 与设置网口的收队列类似,但是它不需要内存池,发包时程序会主动吧要发的mbuf交给网卡,网卡只管发不管从哪来。
-
初始化发送缓冲区
/* Initialize TX buffers */ tx_buffer[portid] = rte_zmalloc_socket("tx_buffer", RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST), 0, rte_eth_dev_socket_id(portid)); if (tx_buffer[portid] == NULL) rte_exit(EXIT_FAILURE, "Cannot allocate buffer for tx on port %u\n", portid); rte_eth_tx_buffer_init(tx_buffer[portid], MAX_PKT_BURST); //错误回调 ret = rte_eth_tx_buffer_set_err_callback(tx_buffer[portid], rte_eth_tx_buffer_count_callback, &port_statistics[portid].dropped); if (ret < 0) rte_exit(EXIT_FAILURE, "Cannot set error callback for tx buffer on port %u\n", portid); //关闭 包类型解析 的功能 ret = rte_eth_dev_set_ptypes(portid, RTE_PTYPE_UNKNOWN, NULL, 0); if (ret < 0) printf("Port %u, Failed to disable Ptype parsing\n", portid);rte_zmalloc_socket声明在DPDK<rte_malloc.h>头文件下,用于在指定 NUMA 节点上分配清零的内存。”0“ 对齐要求,表示默认。RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST)DPDK<rte_ethdev.h>自带的宏,用于计算发送缓冲区需要的内存大小。rte_eth_tx_buffer_init声明在DPDK<rte_ethdev.h>头文件下,用于初始化发送缓冲区。rte_eth_tx_buffer_set_err_callback声明在DPDK<rte_ethdev.h>头文件下,用于设置发送缓冲区的错误回调。rte_eth_tx_buffer_count_callback声明在DPDK<rte_ethdev.h>头文件下,标准回调函数,作用是把发送失败的包数量累加到 userdata 指向的计数器;port_ststistics文件开头定义的全局数组。
rte_eth_dev_set_ptypes声明在DPDK<rte_ethdev.h>头文件下,用于设置网卡要识别哪些包类型。RTE_PTYPE_UNKNOWN,只允许"未知类型",等于全部禁止即关闭所有包类型识别;NULL不需要返回已设置的类型;0表示数组长度为0。
- l2fwd 只做二层转发,只看 MAC 地址,不需要知道三层四层是什么协议。关闭这个功能可以:
- 减少网卡硬件开销;
- 减少 mbuf 字段的写入。
-
启动设备
/* Start device */ ret = rte_eth_dev_start(portid); if (ret < 0) rte_exit(EXIT_FAILURE, "rte_eth_dev_start:err=%d, port=%u\n", ret, portid); printf("done: \n");rte_eth_dev_start声明在DPDK<rte_ethdev.h>头文件下,用于真正启动网卡,开始收发数据。- 在这之前,所有的配置(队列、描述符、offload等)只是写入了软件结构体,调用这个函数后,网卡硬件才真正开始工作,启动后很多配置不能再修改。
-
开启混杂模式
if (promiscuous_on) { ret = rte_eth_promiscuous_enable(portid); if (ret != 0) rte_exit(EXIT_FAILURE, "rte_eth_promiscuous_enable:err=%s, port=%u\n", rte_strerror(-ret), portid); } printf("Port %u, MAC address: " RTE_ETHER_ADDR_PRT_FMT "\n\n", portid, RTE_ETHER_ADDR_BYTES(&l2fwd_ports_eth_addr[portid]));promiscuous_on文件开头定义的全局变量,默认开启rte_eth_promiscuous_enable声明在DPDK<rte_ethdev.h>头文件下,用于开启网卡的混杂模式。- l2fwd 为什么要开混杂模式?
- l2fwd 是转发器,需要处理所有经过的包;
- 这些包的目的MAC可能不是本机,如果不开启,网卡会直接丢弃。
rte_strerror声明在DPDK<rte_string_fns.h>头文件下,用于把错误码转换成可读的错误字符串。RTE_ETHER_ADDR_PRT_FMT声明在DPDK<rte_ether.h>头文件下,用于格式化打印MAC地址。RTE_ETHER_ADDR_BYTES声明在DPDK<rte_ether.h>头文件下,用于提取MAC地址的6个字节,供打印使用。
-
初始化统计信息
/* initialize port stats */ memset(&port_statistics, 0, sizeof(port_statistics));port_statistics文件开头定义的全局数组,确保每个端口的统计计数器都从 0 开始。
-
-
第八阶段:检查链路
//检查是否有可用端口 if (!nb_ports_available) { rte_exit(EXIT_FAILURE, "All available ports are disabled. Please set portmask.\n"); } //检查所有端口的链路状态 check_all_ports_link_status(l2fwd_enabled_port_mask); //启动多核转发 ret = 0; /* launch per-lcore init on every lcore */ rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MAIN); //等待所有工作核结束 RTE_LCORE_FOREACH_WORKER(lcore_id) { if (rte_eal_wait_lcore(lcore_id) < 0) { ret = -1; break; } }-
nb_ports_available在main函数开头定义的局部变量,在第七阶段的循环中累加。 -
check_all_ports_link_status,l2fwd自己定义的函数,等待所有启用的网口物理链路协商完成。 -
rte_eal_mp_remote_launch声明在DPDK<rte_launch.h>的头文件下,用于在所有 lcore 上并行启动指定的函数。参数 传入的值 含义 l2fwd_launch_one_lcore函数指针 在每个 lcore 上执行的函数,l2fwd自定义 NULLNULL 传给函数的参数(这里不需要) CALL_MAIN枚举值 主核也参与执行(不仅仅是工作核) -
DPDK 把 CPU 核分为两类:
- MAIN lcore(主核):运行
main函数的那个核 - WORKER lcore(工作核):其他被
-l启用的核
CALL_MAIN表示主核也要运行l2fwd_launch_one_lcore,参与转发工作。如果传
SKIP_MAIN,主核就只负责管理,不参与转发。 - MAIN lcore(主核):运行
-
-
RTE_LCORE_FOREACH_WORKER声明在DPDK的<rte_lcore.h>头文件下的宏,用于遍历所有工作核(不包含主核)。 -
rte_eal_wait_lcore声明在DPDK的头文件<rte_launch.h>头文件下,用于等待指定的 lcore 执行完毕。<0表示出错。-
为什么只等待 WORKER?
因为主核本身就在这个
main函数里,它也在运行l2fwd_main_loop(死循环)。当用户按 Ctrl+C 触发
force_quit时:- 所有核(包括主核)都从死循环里跳出来
- 主核继续执行
main函数后面的代码 - 工作核执行完
l2fwd_launch_one_lcore返回
所以主核需要在这里等待所有工作核结束,确保大家都安全退出了,再进行清理
-
-
-
第九阶段:清理退出
//停止并关闭所有端口 RTE_ETH_FOREACH_DEV(portid) { if ((l2fwd_enabled_port_mask & (1 << portid)) == 0) continue; printf("Closing port %d...", portid); ret = rte_eth_dev_stop(portid); if (ret != 0) printf("rte_eth_dev_stop: err=%d, port=%d\n", ret, portid); rte_eth_dev_close(portid); printf(" Done\n"); }rte_eth_dev_stop声明在DPDK<rte_ethdev.h>的头文件下,用于停止以太网设备,暂停手法,可以调用rte_eth_dev_start重新启动。rte_eth_dev_close声明在DPDK<rte_ethdev.h>的头文件下,用于关闭以太网设备,安全释放资源,不能再用除非重新配置。- 释放网卡占用的资源
- 如果之前用
-a绑定了网卡,这里会解除绑定
/* clean up the EAL */ rte_eal_cleanup(); printf("Bye...\n"); return ret;rte_eal_cleanup声明在DPDK<rte_eal.h>的头文件下,用于清理EAL环境。- 释放大页内存
- 关闭各种 DPDK 内部资源
- 让系统恢复到程序启动前的状态
2. 头文件部分
- 提供基础功能的标准C库和posix系统调用的头文件
- DPDK的核心库头文件
- 全局变量与宏定义:
3. l2fwd自定义函数
-
check_all_ports_link_status-
变量定义和初始化
-
主循环结构
for (count = 0; count <= MAX_CHECK_TIME; count++)//检查是否被用户强制退出 if (force_quit) return; //初始化本轮检查状态 all_ports_up = 1; //遍历所有DPDK接管的端口 RTE_ETH_FOREACH_DEV(portid) { //只检查用户在-p中启用的端口 if (force_quit) return; if ((port_mask & (1 << portid)) == 0) continue; //获取链路状态 memset(&link, 0, sizeof(link)); ret = rte_eth_link_get_nowait(portid, &link); if (ret < 0) { all_ports_up = 0; if (print_flag == 1) printf("Port %u link get failed: %s\n", portid, rte_strerror(-ret)); continue; } /* print link status if flag set */ if (print_flag == 1) { rte_eth_link_to_str(link_status_text, sizeof(link_status_text), &link); printf("Port %d %s\n", portid, link_status_text); continue; } /* clear all_ports_up flag if any link down */ if (link.link_status == RTE_ETH_LINK_DOWN) { all_ports_up = 0; break; } } /* after finally printing all link status, get out */ if (print_flag == 1) break; if (all_ports_up == 0) { printf("."); fflush(stdout); rte_delay_ms(CHECK_INTERVAL); } /* set the print_flag if all ports up or timeout */ if (all_ports_up == 1 || count == (MAX_CHECK_TIME - 1)) { print_flag = 1; printf("done\n"); }rte_eth_link_get_nowait声明在DPDK的头文件<rte_ethdev.h>下,用于非阻塞地获取链路状态。- 当
print_flag == 1时(所有端口 Up 或者超时了),不再检查状态,而是打印每个端口的最终链路信息。 rte_eth_link_to_str,声明在DPDK的头文件<rte_ethdev.h>下,用于把链路状态结构体转换成可读的字符串。RTE_ETH_LINK_DOWN声明在DPDK的头文件<rte_ethdev.h>下的宏定义,表示网卡链路处于断开状态。如果有端口Down,需要等待一段时间再检查。rte_delay_ms声明在DPDK 的头文件<rte_cycles.h>下,用于毫秒级延时,延时CHECK_INTERVAL秒后再检查。
-
-
l2fwd_launch_one_lcore包装函数,调用
l2fwd_main_loop() -
l2fwd_main_loop-
变量定义和初始化
drain_tsc即100微妙对应的CPU周期数,使用向上取整的除法技巧。
-
获取当前lcore的配置(rx收队列)
RTE_LOG声明在DPDK的头文件<rte_log.h>下的宏定义
-
主循环结构
while (!force_quit) {}-
发送缓冲区刷新 tx
/* Drains TX queue in its main loop. 8< */fa cur_tsc = rte_rdtsc(); /* * TX burst queue drain */ diff_tsc = cur_tsc - prev_tsc; if (unlikely(diff_tsc > drain_tsc)) { for (i = 0; i < qconf->n_rx_port; i++) { 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 is enabled */ if (timer_period > 0) { timer_tsc += diff_tsc; if (unlikely(timer_tsc >= timer_period)) { if (lcore_id == rte_get_main_lcore()) { print_stats(); timer_tsc = 0; } } } prev_tsc = cur_tsc; } /* >8 End of draining TX queue. */unlikely是Linux 内核宏,用于分支预测优化。告诉编译器"这个条件很少为真",让 CPU 流水线优先执行if外面的代码。
这里
unlikely(diff_tsc > drain_tsc)表示:大多数时候时间还没到 100 微秒,不需要刷新。-
注意这里刷新的是发包端口的缓冲区。因为包是收进来后,往另一个端口发的。
-
timer_period文件开头定义的全局变量默认为10s,在l2fwd_parse_args函数中,解析-T参数时赋值。
-
收包和转发RX Burst
/* Read packet from RX queues. 8< */ 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); if (unlikely(nb_rx == 0)) continue; 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); } } /* >8 End of read packet from RX queues. */rte_eth_rx_burst声明在DPDK的头文件<rte_ethdev.h>下,用于从收队列批量取包。rte_prefetch0声明在DPDK的头文件<rte_prefetch.h>下的宏定义,用于预取数据到 CPU 的 L1 缓存,处理当前包时 CPU 就不会等内存了。rte_pktmbuf_mtod声明在DPDK的头文件<rte_mbuf.h>下的宏定义,用于获取 mbuf 中数据区的指针。l2fwd_simple_forwardl2fwd 自己定义的函数,执行真正的转发逻辑(修改 MAC、放入发送缓冲区)。
-
-
-
l2fwd_simple_forward处理每一个收到的包unsigned dst_port; int sent; struct rte_eth_dev_tx_buffer *buffer; //查找目的端口 dst_port = l2fwd_dst_ports[portid]; //如果启用更新MAC地址 if (mac_updating) l2fwd_mac_updating(m, dst_port); buffer = tx_buffer[dst_port]; sent = rte_eth_tx_buffer(dst_port, 0, buffer, m); //统计发送成功的包 if (sent) port_statistics[dst_port].tx += sent;l2fwd_mac_upadtingl2fwd自定义函数rte_eth_tx_buffer声明在DPDK的头文件<rte_ethdev.h>下,用于把一个包放入发送缓冲区。0:包被成功放入缓冲区,等待后续批量发送。> 0:缓冲区满了,触发了一次刷新,返回值是成功发送的包数量。
-
l2fwd_mac_upadtingstruct rte_ether_hdr *eth; void *tmp; //获取以太网指针 eth = rte_pktmbuf_mtod(m, struct rte_ether_hdr *); //把目的MAC地址改成02:00:00:00:00:xx,其中xx是发出端口号 /* 02:00:00:00:00:xx */ tmp = ð->dst_addr.addr_bytes[0]; *((uint64_t *)tmp) = 0x000000000002 + ((uint64_t)dest_portid << 40);/ /* src addr */ rte_ether_addr_copy(&l2fwd_ports_eth_addr[dest_portid], ð->src_addr);rte_pktmbuf_mtod声明在DPDK的头文件<rte_mbuf.h>下的宏定义,用于从 mbuf 获取数据区的指针。02:00:00:00:00:xx写入内存,小端序,低字节在低地址(02)。((uint64_t)dest_portid << 40)左移五个字节,即放在第六个字节的位置。rte_ether_addr_copy声明在DPDK的头文件<rte_ether.h>下,用于复制 MAC 地址。发出端口的真实 MAC 地址复制到包的源 MAC 字段。
扩展
尝试l2fwd-event示例(用 Eventdev 框架调度转发):
该示例在 DPDK 25.11 版本中对软件 Eventdev 的配置确实存在兼容性问题,后续将进一步调控尝试。
后测试发现l2fwd-cat、l2fwd-crypto、l2fwd-macsec等均需要特定的软硬件支持。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)