基于dpdk实现udp接收数据

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.offloadstxmode 等)被隐式初始化为 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),并开启网卡巨型帧卸载能力。


注意事项

  1. 默认零初始化 :未设置的字段(如 txmoderx_adv_conf)均为 0,通常表示"不启用该功能"或"使用驱动程序默认值"。

  2. max_rx_pkt_len 已过时?

    在较新 DPDK 版本(20.11+)中,推荐使用 offloads 标志 DEV_RX_OFFLOAD_JUMBO_FRAME 和独立的 mtu 配置。但该字段仍保留以兼容旧代码。

  3. 静态常量 :定义为 static const 确保每个编译单元有独立的只读副本,避免修改影响其他模块。

  4. 链式指定初始化:可写成更简洁的形式:

一、

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 内存通道数、-a PCI 白名单等)。

  • 初始化大页内存:映射 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_configurerte_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 兼容驱动(如仍被内核驱动 ixgbei40e 等占用)。

    • 未正确加载 vfio-pciigb_uio 模块。

    • 权限不足(需要用 sudo 或配置用户权限)。

    • 系统真的没有网卡。


3. 打印可用端口数量

c

复制代码
printf("nb_sys_ports: %d\n", nb_sys_ports);

因为上一段如果没有可用端口就会退出,所以执行到这段代码时,必然有可用端口。

  • 输出可用端口数,便于调试。例如 nb_sys_ports: 2 表示系统有 2 个网口可用于 DPDK。

  • 注意 :这里使用 %d 打印 uint16_t 没问题,因为值很小。更严格可以用 %uPRIu16

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 缓存,所有分配/释放操作都走全局池(会加锁)。通常设置为 256512 以提升性能,但这里为 0 简化了逻辑。
priv_size 0 每个 mbuf 中用户私有数据区的大小(在 mbuf 结构体之后)。0 表示不需要私有数据。
data_room_size RTE_MBUF_DEFAULT_BUF_SIZE 每个 mbuf 中数据区的大小。该宏通常定义为 20482176(包含 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_i40enet_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 理论上允许端口只配置接收队列而不配置发送队列,适用于纯监听/抓包场景。

  • 但实际是否可行取决于网卡驱动 :部分驱动(如某些版本的 i40eixgbe)可能要求至少一个发送队列,否则 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}
    };
  • 通过复制默认配置,你可以在此基础上修改其他字段(如开启硬件卸载、多队列模式等),但你的程序没有进一步修改,直接使用了该配置。

  • 注意 :未显式初始化的字段(如 txmoderx_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_minnb_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_threshrx_free_threshrx_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_nowaitrte_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));
			}
		}
		
	}
相关推荐
平凡シンプル1 小时前
TCP UDP 网络通信插件 SL-Uniplugin-Tcp-Udp
网络·学习·uniapp·记录
小程序设计1 小时前
Easy-Shop商城小程序漏洞挖掘与防护研究
网络·安全
吴声子夜歌1 小时前
Shell编程实例——bash入门
linux·运维·shell
万联WANFLOW1 小时前
TikTok Shop上线“Sell Across EU”,欧洲社交电商迎来生态重构
网络·业界资讯
爱和冰阔落2 小时前
【Linux】条件变量为什么必须配合互斥锁:从 pthread_cond_wait 到阻塞队列
linux·运维·c++·缓存·中间件·安卓
tedcloud1232 小时前
God‘s Eye View 怎么搭建?在云服务器上部署一个实时 3D 地球可视化平台
linux·服务器·开发语言·后端·rust
星间都市山脉2 小时前
Android16 SystemService.onBootPhase 调用时机
android·java·linux·windows·ubuntu
初願致夕霞2 小时前
Linux 进程间通信(IPC)机制详解:管道、FIFO 与 System V 共享内存
linux·服务器·c++
ynchyong2 小时前
Linux nohup 后台服务合并标准错误输出到一个文件
java·linux·运维