cpp
static const struct rte_eth_conf port_conf_default = {
.rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN}
};
-
定义一个名为
port_conf_default的静态只读结构体变量。 -
类型为
struct rte_eth_conf,这是 DPDK 中以太网设备(网卡)的通用配置结构。 -
初始化时只设置了
rxmode(接收模式)中的max_rx_pkt_len字段,其余字段由编译器自动零初始化。 -
该变量通常作为默认配置模板 ,在调用
rte_eth_dev_configure()前可复制并修改。
2. 关键字与修饰符
| 关键字 | 含义 |
|---|---|
static |
限定作用域为当前编译单元(.c 文件)。若定义在函数内则为局部静态;若在全局则文件内部可见,避免符号冲突。 |
const |
常量限定符,表明变量内容不可修改。存储在只读数据段,提高安全性并允许编译器优化。 |
struct rte_eth_conf |
DPDK 定义的结构体(位于 <rte_ethdev.h>),包含端口的工作模式、卸载能力、流控等配置。 |
3. 结构体 rte_eth_conf 简介
该结构体主要成员(以 DPDK 21.11+ 为例):
cpp
struct rte_eth_conf {
uint32_t link_speeds; /* 链路速度掩码 */
struct rte_eth_rxmode rxmode; /* 接收模式配置 */
struct rte_eth_txmode txmode; /* 发送模式配置 */
uint32_t lpbk_mode; /* 环回模式 */
struct rte_eth_rss_conf rx_adv_conf; /* RSS 高级配置 */
struct rte_eth_txconf tx_adv_conf; /* 发送高级配置 */
struct rte_eth_dcb_conf dcb_capability_conf; /* DCB 配置 */
struct rte_eth_fdir_conf fdir_conf; /* 流定向配置 */
struct rte_eth_intr_conf intr_conf; /* 中断配置 */
};
rxmode 是一个嵌套结构体 struct rte_eth_rxmode,控制接收侧行为:
cpp
struct rte_eth_rxmode {
uint64_t offloads; /* 接收卸载标志位(如校验和卸载) */
uint32_t max_rx_pkt_len; /* 允许接收的最大包长(字节) */
uint16_t split_hdr_size; /* 头部分割大小 */
uint16_t reserved; /* 保留字段 */
enum rte_eth_rx_mq_mode mq_mode; /* 多队列模式 */
};
max_rx_pkt_len:网卡能接收的最大帧长度(不包含 CRC)。通常标准以太网帧为 1518 字节(含 CRC 4 字节),有效载荷最大 1500 字节。但 DPDK 中RTE_ETHER_MAX_LEN定义为 1518 (包含 CRC)或 1514(不含 CRC)?需要确认版本。
在旧版 DPDK(<18.05)中,宏名为
ETHER_MAX_LEN,新版统一为RTE_ETHER_MAX_LEN,其值为 1518 (包括 CRC 和 FCS)。因此设置max_rx_pkt_len = RTE_ETHER_MAX_LEN表示允许接收标准最大以太网帧。
4. 初始化语法(C99 指定初始化器)
cpp
.rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN}
-
.rxmode:指定初始化器的字段名 ,直接给结构体的rxmode成员赋值。 -
{.max_rx_pkt_len = ...}:再对rxmode这个结构体内部的max_rx_pkt_len字段进行指定初始化。 -
未显式初始化的字段(如
rxmode.offloads、txmode等)被隐式初始化为 0(空指针、0 值、false 等),这是 C 语言的静态初始化规则。
这种写法的好处:
-
可读性高:直接看到为哪个成员赋值。
-
顺序无关:不必按照结构体成员声明顺序。
-
部分初始化安全:未指定成员自动清零,避免遗留随机值。
5. RTE_ETHER_MAX_LEN 宏
在 DPDK 头文件 <rte_ether.h> 中定义:
cpp
#define RTE_ETHER_MAX_LEN 1518 /**< 最大帧长度(包含 CRC 和 FCS) */
标准以太网帧结构:
-
目标 MAC(6) + 源 MAC(6) + 类型/长度(2) + 数据(46~1500) + CRC(4) = 64~1518 字节。
-
因此
1518是允许接收的完整以太网帧最大长度(包含 CRC)。
如果希望支持 Jumbo Frame(巨型帧),则需设置为更大值(如 9000),并开启网卡巨型帧卸载能力。
注意事项
-
默认零初始化 :未设置的字段(如
txmode、rx_adv_conf)均为 0,通常表示"不启用该功能"或"使用驱动程序默认值"。 -
max_rx_pkt_len已过时?在较新 DPDK 版本(20.11+)中,推荐使用
offloads标志DEV_RX_OFFLOAD_JUMBO_FRAME和独立的mtu配置。但该字段仍保留以兼容旧代码。 -
静态常量 :定义为
static const确保每个编译单元有独立的只读副本,避免修改影响其他模块。 -
链式指定初始化:可写成更简洁的形式:
一、
cpp
static const struct rte_eth_conf port_conf_default = {
.rxmode.max_rx_pkt_len = RTE_ETHER_MAX_LEN
};
总结
| 部分 | 解释 |
|---|---|
static const |
文件内部可见、只读,安全且优化友好 |
struct rte_eth_conf |
DPDK 以太网端口全局配置结构 |
.rxmode = {...} |
指定初始化接收模式子结构 |
.max_rx_pkt_len = RTE_ETHER_MAX_LEN |
设置最大接收帧长为 1518 字节(标准以太网) |
| 未初始化字段 | 自动置零,表示使用默认/关闭状态 |
这段代码是 DPDK 程序初始化网卡时常用的默认配置模板,只保证了能接收标准大小帧,其他高级功能(如多队列、硬件卸载、流控等)需要额外配置。
二、
cpp
int main(int argc, char *argv[]){
if(rte_eal_init(argc,argv)<0){
rte_exit(EXIT_FAILURE, "Error with EAL init\n");
}
1. rte_eal_init(argc, argv) 的作用
rte_eal_init 是 DPDK 提供的初始化函数,负责:
-
解析并处理 DPDK 专用的命令行参数 (如
-c指定核掩码、-n内存通道数、-aPCI 白名单等)。 -
初始化大页内存:映射 hugepage、创建 mempool 所需的内存区域。
-
检测并初始化可用的网络设备(通过 UIO/VFIO 驱动绑定)。
-
设置 CPU 亲和性与 NUMA 感知:为后续分配内存、创建线程奠定基础。
-
启动必要的内部服务线程(如中断处理、日志、定时器)。
参数:
argc/argv:直接透传main函数的命令行参数。EAL 会消费掉它认识的选项,并将剩余参数(应用程序自定义的)保留在rte_eal_init返回后的argv中。
返回值:
-
成功 :返回被 DPDK EAL 解析并移除的参数个数(即从
argv中"吃掉"的参数数量)。 -
失败 :返回负数(通常是
-1或-ENOTSUP等错误码)。
2.失败时的处理:rte_exit
c
rte_exit(EXIT_FAILURE, "Error with EAL init\n");
rte_exit 是 DPDK 提供的辅助函数,等价于:
-
打印用户给出的错误消息(到标准错误输出)。
-
调用
rte_eal_cleanup()进行必要的清理(释放资源、关闭设备)。 -
最终调用
exit(EXIT_FAILURE)终止进程。
注意 :使用 rte_exit 而不是 exit 可以确保 DPDK 内部的资源被正确释放,避免内存泄漏或设备状态异常。
3. 常见初始化失败原因
| 原因 | 说明 |
|---|---|
| 大页内存未配置 | 系统未挂载 hugetlbfs 或 /sys/kernel/mm/hugepages 中页数不足。 |
| 权限不足 | 未以 root 运行,或进程没有 VFIO/UIO 设备访问权限(需用 sudo 或设置 CAP_IPC_LOCK)。 |
| 参数错误 | 提供了 EAL 不认识的选项,或数值超出范围(如核掩码包含不存在的 CPU)。 |
| PCI 设备绑定失败 | 网卡未绑定到 igb_uio/vfio-pci 驱动,或被其他内核驱动占用。 |
| 内存通道数配置不当 | -n 指定的通道数与实际硬件不符(对某些平台非致命,但可能警告)。 |
4.注意事项
-
必须第一个调用 :在
rte_eal_init成功返回前,几乎所有其他 DPDK API(如rte_eth_dev_configure、rte_mempool_create)都不可用。 -
只能调用一次:多次调用会导致未定义行为(通常崩溃)。
-
多进程场景 :DPDK 支持主/从进程,EAL 初始化参数需要配合
--proc-type使用,此处略。
5.总结
| 组件 | 功能 |
|---|---|
rte_eal_init |
一站式初始化 DPDK 运行环境(内存、设备、CPU 等) |
返回值 <0 |
表示初始化失败,无法继续 |
rte_exit |
打印错误、清理资源、退出进程 |
这段代码是任何 DPDK 应用程序的必要开场白,没有它就无法使用 DPDK 的高性能数据包处理能力。总之,dpdk通过这段代码来完成初始化。
三、
cpp
uint16_t nb_sys_ports = rte_eth_dev_count_avail();
if(nb_sys_ports == 0){
rte_exit(EXIT_FAILURE, "No Support eth found\n");
}
printf("nb_sys_ports: %d\n", nb_sys_ports);
1. rte_eth_dev_count_avail() 详解
| 项目 | 说明 |
|---|---|
| 函数原型 | uint16_t rte_eth_dev_count_avail(void) |
| 所在头文件 | <rte_ethdev.h> |
| 返回值 | 系统中可用的 DPDK 以太网端口数量 |
| "可用"定义 | 端口已被 EAL 成功探测、驱动程序已绑定、设备未处于异常状态。通常对应通过 PCIe 总线发现的、已绑定到 igb_uio / vfio-pci 的网卡。 |
| 注意 | 该函数只统计可用端口,不包括已被其他 DPDK 进程占用的端口(在多进程模式下)。 |
在你的程序之前,rte_eal_init() 会扫描 PCI 总线,根据绑定的驱动(如 vfio-pci)初始化网卡设备。成功初始化的网卡就会被计入 rte_eth_dev_count_avail()。
2. 错误检查:if (nb_sys_ports == 0)
c
if (nb_sys_ports == 0) {
rte_exit(EXIT_FAILURE, "No Support eth found\n");
}
-
含义 :如果没有找到任何可用的以太网端口,程序就无法继续(因为后续代码硬编码使用
gDpdkPortId = 0,且没有发端需求,纯收包必须有至少一个端口)。 -
处理 :调用
rte_exit()打印错误信息并终止进程,返回EXIT_FAILURE。 -
常见失败原因:
-
没有网卡绑定到 DPDK 兼容驱动(如仍被内核驱动
ixgbe、i40e等占用)。 -
未正确加载
vfio-pci或igb_uio模块。 -
权限不足(需要用
sudo或配置用户权限)。 -
系统真的没有网卡。
-
3. 打印可用端口数量
c
printf("nb_sys_ports: %d\n", nb_sys_ports);
因为上一段如果没有可用端口就会退出,所以执行到这段代码时,必然有可用端口。
-
输出可用端口数,便于调试。例如
nb_sys_ports: 2表示系统有 2 个网口可用于 DPDK。 -
注意 :这里使用
%d打印uint16_t没问题,因为值很小。更严格可以用%u或PRIu16。
4.总结
| 代码片段 | 作用 |
|---|---|
rte_eth_dev_count_avail() |
获取 DPDK 可用网口数量 |
if (nb_sys_ports == 0) |
确保至少有一个网口,否则退出 |
printf(...) |
输出端口数量,便于调试 |
后续使用 gDpdkPortId = 0 |
假设第一个可用端口存在(合理,因为数量≥1) |
这段代码是整个 DPDK 程序运行时环境检查的关键一环,确保后续的端口配置操作有设备可用。
cpp
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbufpool", NUM_MBUFS, 0, 0,
RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
if(!mbuf_pool){
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
}
cpp
#define NUM_MBUFS 2048 // 之前定义的宏
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
"mbufpool", // 内存池名称
NUM_MBUFS, // mbuf 数量
0, // 缓存大小(per-lcore cache)
0, // 私有数据大小
RTE_MBUF_DEFAULT_BUF_SIZE, // 每个 mbuf 的数据区大小
rte_socket_id() // NUMA 节点 ID
);
if (!mbuf_pool) {
rte_exit(EXIT_FAILURE, "Could not create mbuf pool\n");
}
1. rte_pktmbuf_pool_create() 函数详解
| 参数 | 值 | 含义 |
|---|---|---|
name |
"mbufpool" |
内存池的唯一标识名称,用于调试和统计(会在 /dev/hugepages 或共享内存中显示)。 |
n |
NUM_MBUFS (2048) |
内存池中 mbuf 的总数量。这个值决定了能同时容纳多少个数据包。如果接收突发流量很大,内存池可能耗尽,导致丢包。 |
cache_size |
0 |
每个 CPU 核心的本地缓存大小。若为 0,则禁用 per-lcore 缓存,所有分配/释放操作都走全局池(会加锁)。通常设置为 256 或 512 以提升性能,但这里为 0 简化了逻辑。 |
priv_size |
0 |
每个 mbuf 中用户私有数据区的大小(在 mbuf 结构体之后)。0 表示不需要私有数据。 |
data_room_size |
RTE_MBUF_DEFAULT_BUF_SIZE |
每个 mbuf 中数据区的大小。该宏通常定义为 2048 或 2176(包含 headroom),足够容纳标准以太网帧(1518 字节)加上一些预留空间。 |
socket_id |
rte_socket_id() |
NUMA 节点 ID。rte_socket_id() 返回当前执行线程所在的 CPU 插槽编号,确保内存池分配在离当前 CPU 最近的本地内存上,避免跨 NUMA 访问延迟。 |
返回值 :成功返回指向 rte_mempool 结构体的指针;失败返回 NULL。
2. 为什么需要 mbuf 内存池?
-
高效分配:DPDK 绕过内核,自己管理数据包内存。预分配一个 mbuf 池,可以在数据平面快速分配/释放(通常是无锁的 per-lcore 缓存 + 批量操作)。
-
避免动态内存分配 :在高速收包循环中(如每秒百万包),调用
malloc是不可接受的。内存池使用环形缓冲区(ring)或无锁栈实现 O(1) 分配。 -
数据包生命周期:从网卡收到的每个包都存放在一个 mbuf 中;处理完后需要将 mbuf 释放回池中,否则池会耗尽。
在你的程序中,这个 mbuf_pool 后续被用于接收队列的设置:
c
rte_eth_rx_queue_setup(gDpdkPortId, 0, 128,
rte_eth_dev_socket_id(gDpdkPortId),
NULL, mbuf_pool);
-
网卡收到数据包时,驱动程序会自动从
mbuf_pool中分配一个 mbuf 来存放数据。 -
如果池中无可用 mbuf,网卡会丢弃该包并统计丢包计数。
3. 错误检查 if (!mbuf_pool)
-
如果
rte_pktmbuf_pool_create返回NULL,说明创建失败。 -
常见失败原因:
-
大页内存不足 :系统预留的大页内存不够存放
NUM_MBUFS * (mbuf结构体大小+数据区大小)。例如每个 mbuf 约 2KB,2048 个需要约 4MB,加上内存池管理开销,一般都能满足。但如果大页仅配置了 2MB,且被其他程序占用,可能失败。 -
内存池名称冲突:同一 DPDK 进程中已经存在同名内存池(很少见)。
-
socket_id 无效 :
rte_socket_id()返回的 NUMA 节点不存在或该节点上没有足够内存。
-
-
失败后调用
rte_exit()打印错误并退出程序。
4. 与之前代码的关联
-
在调用此函数之前,程序已经成功执行了
rte_eal_init(),大页内存已经初始化好。 -
之后会调用
rte_eth_dev_configure()、rte_eth_rx_queue_setup()等,这些函数都需要一个有效的 mbuf 池。 -
在无限循环的收包中,程序通过
rte_eth_rx_burst()获取 mbuf,解析后没有释放 mbuf ,这是一个内存泄漏(后续应该调用rte_pktmbuf_free())。如果不释放,最终池中 mbuf 耗尽,所有新收到的包都会被丢弃。
5.总结
| 代码部分 | 作用 |
|---|---|
rte_pktmbuf_pool_create(...) |
创建一个预分配的 mbuf 内存池,供网卡接收数据包使用 |
"mbufpool" |
内存池名称,便于识别 |
NUM_MBUFS = 2048 |
最多同时容纳 2048 个数据包 |
cache_size = 0 |
关闭 per-lcore 缓存(简化,但性能较差) |
priv_size = 0 |
不需要私有数据 |
RTE_MBUF_DEFAULT_BUF_SIZE |
每个 mbuf 数据区大小(通常 2048 字节) |
rte_socket_id() |
将内存池分配到当前 CPU 所在的 NUMA 节点 |
if (!mbuf_pool) |
检查创建是否成功,失败则退出程序 |
这个 mbuf 池是整个 DPDK 收包流程的数据基础,没有它网卡无法存放收到的数据包。在后续的 rte_eth_rx_queue_setup 中会明确告诉网卡驱动程序使用这个池来分配 mbuf。用mbuf池来接受dpdk的数据。
cpp
struct rte_eth_dev_info dev_info;
rte_eth_dev_info_get(gDpdkPortId, &dev_info);
这两行代码用于获取指定以太网端口的详细信息 ,并将结果存储在 dev_info 结构体中。
1. struct rte_eth_dev_info 结构体
这个结构体定义在 <rte_ethdev.h> 中,用于保存以太网设备的各种能力与限制信息,包括:
| 字段(部分) | 含义 |
|---|---|
driver_name |
驱动名称(如 net_i40e、net_ixgbe) |
if_index |
接口索引 |
min_rx_bufsize |
最小接收缓冲区大小 |
max_rx_pktlen |
最大接收包长(硬件支持) |
max_rx_queues / max_tx_queues |
支持的最大收发队列数 |
rx_offload_capa / tx_offload_capa |
硬件卸载能力(如校验和、TSO) |
pci_dev |
关联的 PCI 设备信息 |
作用 :程序可以根据这些信息来动态调整配置,例如如果设备只支持 2 个队列,就不能配置 4 个;如果设备不支持某种 offload,就不能开启。
2. rte_eth_dev_info_get() 函数
c
int rte_eth_dev_info_get(uint16_t port_id, struct rte_eth_dev_info *dev_info);
-
参数:
-
port_id:要查询的 DPDK 端口 ID(你的程序使用gDpdkPortId = 0)。 -
dev_info:指向rte_eth_dev_info结构体的指针,用于接收信息。
-
-
返回值:成功返回 0;失败返回负数(如端口 ID 无效)。
总结
| 代码 | 作用 |
|---|---|
struct rte_eth_dev_info dev_info; |
声明一个结构体变量,用于存放设备详细信息 |
rte_eth_dev_info_get(gDpdkPortId, &dev_info); |
从 DPDK 获取端口 0 的设备能力信息 |
| 未检查返回值 | 小瑕疵,可能导致使用未初始化的结构体(但你的程序未使用,所以安全) |
后续未使用 dev_info |
该行可能是调试遗留或预留扩展,不影响功能,但可以移除或完善 |
cpp
const int num_rx_queues = 1;
const int num_tx_queues = 0;
struct rte_eth_conf port_conf = port_conf_default;
if (rte_eth_dev_configure(gDpdkPortId, num_rx_queues, num_tx_queues, &port_conf) < 0) {
rte_exit(EXIT_FAILURE, "Could not configure\n");
}
1. rte_eth_dev_configure() 函数的作用
函数原型:
c
int rte_eth_dev_configure(uint16_t port_id, uint16_t nb_rx_queue, uint16_t nb_tx_queue,
const struct rte_eth_conf *dev_conf);
-
作用 :对指定的以太网端口进行基础配置,包括:
-
接收队列的数量
-
发送队列的数量
-
端口的全局配置(如接收模式、卸载能力、流控等)
-
-
调用时机 :必须在端口启动(
rte_eth_dev_start)之前调用,通常在初始化阶段。 -
返回值:成功返回 0;失败返回负数(如参数无效、硬件不支持等)。
2. 参数详解
| 参数 | 值 | 含义 |
|---|---|---|
port_id |
gDpdkPortId(全局变量,值为 0) |
要配置的 DPDK 端口 ID。你的程序之前已通过 rte_eth_dev_count_avail() 确认至少有一个可用端口,且假设端口 0 存在。 |
nb_rx_queue |
1 |
为该端口配置 1 个接收队列。你的程序只用一个队列收包,简化处理。 |
nb_tx_queue |
0 |
配置 0 个发送队列。因为程序只接收数据包,不发送任何报文,所以不需要发送队列。 |
dev_conf |
&port_conf |
指向端口配置结构体的指针。port_conf 从默认配置 port_conf_default 复制而来,该默认配置只设置了 rxmode.max_rx_pkt_len = RTE_ETHER_MAX_LEN(1518 字节),其余字段均为 0。 |
3. 为什么 num_tx_queues = 0 是允许的?
-
DPDK 理论上允许端口只配置接收队列而不配置发送队列,适用于纯监听/抓包场景。
-
但实际是否可行取决于网卡驱动 :部分驱动(如某些版本的
i40e、ixgbe)可能要求至少一个发送队列,否则rte_eth_dev_configure会返回错误。如果你的程序运行时在此处失败,可以尝试改为num_tx_queues = 1(即使不使用,驱动也可能需要)。 -
你的程序后面没有调用任何发送相关的函数(如
rte_eth_tx_burst),所以0在逻辑上是合理的。
4. port_conf 的初始化
c
struct rte_eth_conf port_conf = port_conf_default;
-
port_conf_default是你之前定义的静态常量:c
static const struct rte_eth_conf port_conf_default = { .rxmode = {.max_rx_pkt_len = RTE_ETHER_MAX_LEN} }; -
通过复制默认配置,你可以在此基础上修改其他字段(如开启硬件卸载、多队列模式等),但你的程序没有进一步修改,直接使用了该配置。
-
注意 :未显式初始化的字段(如
txmode、rx_adv_conf)被自动置零,表示禁用这些高级功能。
5. 错误处理
c
if (rte_eth_dev_configure(...) < 0) {
rte_exit(EXIT_FAILURE, "Could not configure\n");
}
-
如果配置失败(返回负数),程序立即打印错误信息并退出。
-
常见失败原因:
-
端口 ID 无效(例如
gDpdkPortId = 0但系统中可用端口号不是从 0 开始?DPDK 会连续编号,所以只要nb_sys_ports > 0,端口 0 就存在)。 -
请求的队列数超过硬件支持的最大值(你的程序虽然之前获取了
dev_info但未使用,可能max_rx_queues小于 1 的情况极少,或者max_tx_queues为 0 但不允许 0)。 -
配置参数与硬件能力冲突(例如设置了硬件不支持的卸载标志,但你的配置全为 0,一般不会冲突)。
-
端口已经被其他进程占用(DPDK 多进程模式下)。
-
6. 与前后代码的关联
前置依赖
-
在这段代码之前,必须已经调用
rte_eal_init()和rte_eth_dev_count_avail(),确保 EAL 已初始化且存在可用端口。 -
创建 mbuf 池的代码可以放在配置之前或之后,但通常在配置之前创建,因为后续设置接收队列时需要传入 mbuf 池。你的程序正是这样做的。
后续步骤
-
配置成功后,程序紧接着调用
rte_eth_rx_queue_setup()为队列 0 设置具体的接收环大小、mbuf 池等。 -
然后调用
rte_eth_dev_start()启动端口,开始收包。 -
如果此处配置失败,后续所有操作都不会执行,程序直接退出。
7.总结
| 代码部分 | 作用 |
|---|---|
const int num_rx_queues = 1; |
定义接收队列数量为 1 |
const int num_tx_queues = 0; |
定义发送队列数量为 0(只收不发) |
struct rte_eth_conf port_conf = port_conf_default; |
使用默认配置(最大包长 1518 字节) |
rte_eth_dev_configure(...) |
将上述配置应用到端口 0 |
错误检查 <0 |
配置失败时退出程序 |
这段代码是 DPDK 程序中将抽象的端口配置具体化的步骤,它告诉 DPDK 该端口将使用多少个收发队列以及基本的端口行为。正确的配置是后续成功启动端口并接收数据包的前提。如果你的程序运行时在这个阶段失败,可以检查驱动是否支持 0 个发送队列,或者尝试增加发送队列数量。
cpp
if(rte_eth_rx_queue_setup(gDpdkPortId,0, 128, rte_eth_dev_socket_id(gDpdkPortId),
NULL, mbuf_pool) < 0){
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
}
1. rte_eth_rx_queue_setup() 函数的作用
函数原型:
c
int rte_eth_rx_queue_setup(uint16_t port_id, uint16_t rx_queue_id,
uint16_t nb_rx_desc, unsigned int socket_id,
const struct rte_eth_rxconf *rx_conf,
struct rte_mempool *mb_pool);
-
作用:为指定的以太网端口配置一个具体的接收队列。
-
调用时机 :必须在
rte_eth_dev_configure()之后、rte_eth_dev_start()之前调用。 -
返回值:成功返回 0;失败返回负数(如参数无效、内存不足等)。
2. 参数详解
| 参数 | 传入值 | 含义 |
|---|---|---|
port_id |
gDpdkPortId(=0) |
目标端口 ID |
rx_queue_id |
0 |
接收队列索引。因为之前配置了 num_rx_queues = 1,所以有效队列 ID 为 0。 |
nb_rx_desc |
128 |
接收环的描述符数量(即硬件队列深度)。表示网卡可以在该队列中缓存最多 128 个待处理的接收包。 |
socket_id |
rte_eth_dev_socket_id(gDpdkPortId) |
该端口所在的 NUMA 节点 ID。确保后续分配的内存(如 mbuf)与网卡在同一节点,避免跨 NUMA 访问延迟。 |
rx_conf |
NULL |
接收队列的额外配置(如卸载标志、阈值等)。传入 NULL 表示使用驱动默认的接收队列配置。 |
mb_pool |
mbuf_pool |
指向之前创建的内存池的指针。网卡收到数据包时会从这个池中分配 mbuf 来存储数据。 |
3. 各参数深入讲解
3.1 nb_rx_desc = 128
-
含义:接收队列的环形缓冲区大小(描述符数量)。每个描述符对应一个可以存放数据包的 mbuf。
-
影响:
-
值越大,网卡能缓存的未处理包越多,能更好地应对突发流量,但会占用更多内存。
-
值越小,内存占用少,但容易因 CPU 处理不及时导致丢包。
-
-
取值范围 :通常受硬件限制,可以通过
rte_eth_dev_info_get()获取dev_info.rx_desc_lim.nb_min和nb_max。你的程序未做检查,直接使用 128,这是一个常见且较安全的值(很多网卡支持 128~4096)。 -
注意 :该值不是 mbuf 池的大小。
mbuf_pool应足够大(你的NUM_MBUFS = 2048),以容纳所有队列描述符所需的 mbuf 以及额外的缓冲。
3.2 socket_id = rte_eth_dev_socket_id(gDpdkPortId)
-
作用:获取该网卡所连接的 NUMA 节点。例如,若端口 0 位于 PCIe 插槽连接到 CPU socket 0,则返回 0。
-
为什么重要:将 mbuf 池分配在网卡所在的同一 socket 上,可以避免数据在跨 NUMA 总线上传输,显著降低延迟并提高吞吐量。
-
替代方案 :也可以直接用
rte_socket_id(),但那样使用的是当前执行线程所在的 socket,可能与网卡不在同一个节点。
3.3 rx_conf = NULL
-
含义:使用驱动提供的默认接收队列配置。驱动通常会设置合适的接收阈值、中断模式等。
-
高级用法 :你可以创建
struct rte_eth_rxconf结构体并设置rx_thresh、rx_free_thresh、rx_drop_en等字段来精细控制队列行为。
3.4 mb_pool = mbuf_pool
-
关键作用 :网卡 DMA 引擎会将接收到的数据包直接写入从该内存池分配的 mbuf 中。你的程序之前创建的
mbuf_pool大小为 2048 个 mbuf,每个 mbuf 数据区大小为RTE_MBUF_DEFAULT_BUF_SIZE(通常 2048 字节),足够存放标准以太网帧。 -
注意事项:一个内存池可以被多个接收队列共享,但通常每个队列使用独立的池以避免竞争(如果池支持多生产者/多消费者)。你的程序只有一个队列,共享没问题。
4. 错误处理
c
if (rte_eth_rx_queue_setup(...) < 0) {
rte_exit(EXIT_FAILURE, "Could not setup RX queue\n");
}
-
常见失败原因:
-
port_id无效或端口未配置。 -
rx_queue_id超出之前配置的队列数量(你配置了 1 个队列,ID 0 有效)。 -
nb_rx_desc超出硬件支持的范围(如某些网卡最小 64,最大 4096,128 通常有效)。 -
mb_pool无效(例如为NULL或已被销毁)。 -
内存不足(如无法为接收环分配描述符所需的内存)。
-
设备或驱动不支持所请求的配置。
-
5. 与前后代码的关联
前置依赖
-
必须已成功调用
rte_eth_dev_configure(),因为该函数确定了接收队列的数量。 -
必须已创建
mbuf_pool,因为网卡需要从池中预分配描述符的 mbuf。 -
建议(但非必须)已调用
rte_eth_dev_info_get()来验证nb_rx_desc的合法性。
总结
| 代码 | 作用 |
|---|---|
rte_eth_rx_queue_setup(...) |
为端口 0 的队列 0 配置接收环(128 个描述符) |
rte_eth_dev_socket_id(gDpdkPortId) |
获取网卡所在的 NUMA 节点,确保本地内存访问 |
NULL |
使用驱动默认的接收队列高级配置 |
mbuf_pool |
网卡将接收的数据包放入从这个池分配的 mbuf 中 |
错误检查 <0 |
配置失败时退出程序 |
这段代码是 DPDK 零拷贝收包路径的核心初始化环节 ,它把之前创建的 mbuf 池和硬件接收队列绑定起来,使得后续 rte_eth_rx_burst 能够无中断、高效率地从网卡获取数据包。
cpp
if(rte_eth_dev_start(gDpdkPortId) < 0)
{
rte_exit(EXIT_FAILURE, "Could not start\n");
}
printf("dev start success\n");
1. rte_eth_dev_start() 函数的作用
函数原型:
c
int rte_eth_dev_start(uint16_t port_id);
-
作用:启动指定的以太网端口,使其开始处理数据包的接收和发送。
-
调用时机 :必须在端口配置(
rte_eth_dev_configure)和所有队列设置(rte_eth_rx_queue_setup/rte_eth_tx_queue_setup)完成之后调用。 -
返回值:成功返回 0;失败返回负数(如硬件初始化失败、链路未建立等)。
2. 启动端口时发生了什么?
调用 rte_eth_dev_start 后,DPDK 驱动会执行一系列硬件初始化和使能操作:
| 操作 | 说明 |
|---|---|
| 使能接收/发送队列 | 将之前配置的接收环和发送环写入网卡寄存器,通知网卡 DMA 引擎可以使用这些队列。 |
| 预分配描述符的 mbuf | 对于接收队列,驱动会从绑定到队列的 mempool 中预先取出 nb_rx_desc 个 mbuf,将其物理地址填充到接收环的描述符中,供网卡写入数据。 |
| 更新链路状态 | 启动后网卡会尝试建立物理链路(如协商速率、双工模式)。可以通过 rte_eth_link_get 获取链路状态。 |
| 启用中断或轮询模式 | 根据配置,网卡可能开始产生接收中断(如果使用中断模式)或直接等待轮询。DPDK 通常使用轮询,所以启动后网卡会将收到的数据包直接写入预先填充好的 mbuf。 |
| 清除设备错误状态 | 如果端口之前因错误而停止,启动会尝试重置并恢复。 |
关键点 :启动成功后,网卡硬件已经准备好接收数据包,并且接收队列中已经填充了可用的 mbuf。此时调用 rte_eth_rx_burst 就能立刻从队列中取到包。
3. 错误处理
c
if (rte_eth_dev_start(gDpdkPortId) < 0) {
rte_exit(EXIT_FAILURE, "Could not start\n");
}
-
如果启动失败(返回负数),程序立即打印错误信息并退出。
-
常见失败原因:
-
端口 ID 无效或端口之前未配置。
-
内存不足:无法为接收队列预分配足够的 mbuf(例如
mbuf_pool大小小于nb_rx_desc)。 -
硬件初始化失败:例如网卡固件问题、PCI 配置空间错误。
-
链路协商失败(某些驱动在启动时要求链路必须 up,但大多数驱动允许启动后链路 down)。
-
驱动或硬件不支持当前配置(例如要求了过多的队列)。
-
注意 :即使启动成功,链路可能尚未 up(例如网线未插)。你的程序并未检查链路状态,直接进入收包循环,这会导致 rte_eth_rx_burst 一直返回 0。通常建议在启动后轮询链路状态,直到 link_status 为 up。
4. 启动成功后的标志性输出
c
printf("dev start success\n");
-
这是一个用户友好的提示,表明端口已经启动,后续可以开始收包。
-
在 DPDK 典型应用中,这一步之后通常会打印端口 MAC 地址、链路速度等信息,便于调试。
5. 与前后代码的关联
前置依赖
-
必须已成功调用
rte_eth_dev_configure(配置了收发队列数量)。 -
必须已成功调用
rte_eth_rx_queue_setup(至少一个接收队列,因为你的程序需要收包)。 -
虽然你的程序没有发送队列,但如果有发送需求,也需要调用
rte_eth_tx_queue_setup并在启动前完成。
后续操作
-
启动后,程序进入无限循环,调用
rte_eth_rx_burst从队列 0 批量接收数据包。 -
如果端口启动失败,后续的收包循环不会执行,程序直接退出。
6. 常见问题与最佳实践
| 问题 | 说明 | 建议 |
|---|---|---|
| 未检查链路状态 | 启动后链路可能仍为 down,导致收不到任何包。 | 调用 rte_eth_link_get_nowait 或 rte_eth_link_get 等待链路 up。 |
| 启动后修改配置 | 端口启动后不能重新配置队列数量或卸载能力。 | 如需修改,必须先调用 rte_eth_dev_stop,重新配置后再启动。 |
| 多端口顺序启动 | 如果程序管理多个端口,通常逐个启动。 | 可以并行启动,但要注意资源竞争。 |
| 启动失败后的清理 | 你的程序直接 rte_exit,没有调用 rte_eth_dev_stop 或释放资源。 |
对于生产程序,应适当清理已分配的资源。 |
7. 启动后网卡的状态变化
| 时间点 | 接收队列状态 | 可调用 rte_eth_rx_burst |
|---|---|---|
| 配置后、启动前 | 描述符未填充,硬件未使能 | 不可用(可能返回 0 或错误) |
| 启动后 | 描述符已填充 mbuf,硬件开始接收 | 可以,返回收到的包数 |
停止后 (rte_eth_dev_stop) |
硬件停止接收,描述符可能被清空 | 不可用 |
总结
| 代码 | 作用 |
|---|---|
rte_eth_dev_start(gDpdkPortId) |
启动端口 0,使网卡开始接收数据包 |
if < 0 |
启动失败时退出程序 |
printf("dev start success\n") |
提示用户端口已启动 |
这段代码标志着数据平面正式开始工作 。没有这一步,后续的 rte_eth_rx_burst 将无法收到任何数据包。启动成功后,网卡硬件已经准备就绪,程序就可以零拷贝、高吞吐地捕获网络流量了。
cpp
while(1){
struct rte_mbuf *mbufs[BURST_SIZE];
unsigned nb_recvd = rte_eth_rx_burst(gDpdkPortId, 0, mbufs, BURST_SIZE);
if(nb_recvd > BURST_SIZE){
rte_exit(EXIT_FAILURE, "Error with rte_eth_rx_burst\n");
}
unsigned i =0;
for(i=0; i<nb_recvd; i++){
struct rte_ether_hdr *ehdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);
if(ehdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)){
continue;
}
struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));
if(iphdr->next_proto_id == IPPROTO_UDP){
struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);
uint16_t length = udphdr->dgram_len;
printf("length: %d, content: %s\n", length, (char *)(udphdr+1));
}
}
}