欢迎光临
我们一直在努力

【DPDK实战】编写一个高性能 UDP 抓包程序

在高性能网络编程领域,DPDK (Data Plane Development Kit) 是绕不开的一座大山。它通过绕过操作系统内核(Kernel Bypass),直接在用户态接管网卡,实现了极高的数据包处理效率。

很多初学者配置好环境后,面对复杂的 API 无从下手。今天这篇文章,我们将从零开始,编写一个基于 DPDK 的 用户态协议栈雏形(ustack)。

本文目标: 实现一个程序,能够从网卡捕获 IPv4 下的 UDP 数据包,并解析打印出 Payload 内容。

核心概念:为什么 DPDK 这么快?

在看代码之前,我们需要理解 DPDK 的三个核心,这也是我们代码中各个模块存在的意义:

  • UIO / VFIO(绕过内核): 传统 socket 编程,数据要从网卡 -> 驱动 -> 内核协议栈 -> 用户空间,发生了多次内存拷贝。DPDK 直接把网卡映射到用户空间,零拷贝(Zero Copy)直接读写。

  • Hugepages & Mempool(大页内存与内存池): 为了避免频繁的 malloc/free 造成性能抖动,DPDK 在启动时就申请好一大块内存(大页),切割成固定大小的 mbuf 放在池子里,随用随取,用完归还。

  • Polling Mode(轮询模式): 传统网卡使用中断(Interrupt)通知 CPU,高并发下 CPU 会被中断淹没。DPDK 使用 while(1) 死循环不断查询网卡:“有包吗?有包吗?”,虽然占用了 CPU,但消除了中断开销,吞吐量极大。

  • 代码全解析

    我们将代码分为三个部分:环境初始化、网卡配置、业务循环。

    1. 头文件与宏定义

    #include <stdio.h> // 标准输入输出库,用于 printf
    #include <rte_eal.h> // DPDK 核心库:EAL (Environment Abstraction Layer) 环境抽象层
    #include <rte_ethdev.h> // DPDK 网卡设备库:提供网卡配置、收发包 API
    #include <arpa/inet.h> // 提供网络字节序转换等函数 (如 htons)

    // 指定我们要使用的网卡端口 ID,0 代表系统识别到的第一个 DPDK 网卡
    int global_portid = 0;

    // 内存池大小:申请 4096 个 mbuf (内存块)
    // 如果包处理得慢,或者收包太快,这个池子可能会枯竭,导致丢包
    #define NUM_MBUFS 4096

    // 批量处理大小:每次从网卡“抢” 128 个包
    #define BURST_SIZE 128

    注意:BURST_SIZE 设置为 128 或 32 是常见做法。批量处理可以分摊函数调用的开销(类似批发比零售便宜)。

    2. 网卡初始化函数 (ustack_init_port)

    这个函数负责告诉网卡:“我要怎么用你”。

    // 网卡的基础配置结构体
    static const struct rte_eth_conf port_conf_default = {
    // 设置接收模式:最大包长度为以太网标准最大长度 (1518字节)
    // 注意:旧版本是用 RTE_ETH_MAX_PKT_LEN,新版本用 RTE_ETHER_MAX_LEN
    .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
    };

    // [函数:初始化网卡端口]
    // static 关键字:表示这是内部函数,防止对外暴露符号,避免命名冲突
    static int ustack_init_port(struct rte_mempool *mbuf_pool){

    // 1. 查询系统里有几个可用的 DPDK 网卡
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();
    if(nb_sys_ports == 0){
    rte_exit(EXIT_FAILURE, "No available ports\\n"); // 如果没有网卡,直接报错退出
    }

    // 2. 配置网卡参数
    const int num_rx_queues = 1; // 启用 1 个接收队列 (收货口)
    const int num_tx_queues = 0; // 启用 0 个发送队列 (只收不发,做抓包工具)

    // 下发配置给 global_portid 号网卡
    rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);

    // 3. 绑定接收队列 (RX Queue) 和 内存池 (Mempool)
    // 意思:0 号网卡的 0 号队列,你以后收到的包,都往 mbuf_pool 里的空箱子里装
    // 参数 128 是指接收描述符的数量 (Ring Size),即网卡硬件缓冲区的大小
    if(rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0){
    rte_exit(EXIT_FAILURE, "Could not setup RX queue\\n");
    }

    // 4. 正式启动网卡 (Link Up)
    // 这一步之后,网卡指示灯通常会亮起,开始工作
    if(rte_eth_dev_start(global_portid) < 0) {
    rte_exit(EXIT_FAILURE,"Could not start\\n");
    }

    return 0; // 初始化成功
    }

    3. Main 函数:EAL 初始化与内存池

    int main(int argc, char *argv[]){

    // [Step 1] EAL 初始化 (Environment Abstraction Layer)
    // 这是 DPDK 的入口,负责接管 CPU、内存和网卡硬件
    // 如果这一步失败,说明环境没配好 (比如没有大页内存,或者没加载 vfio-pci 驱动)
    if(rte_eal_init(argc,argv) < 0){
    rte_exit(EXIT_FAILURE, "EAL initialization failed\\n");
    }

    // [Step 2] 创建内存池 (Mempool)
    // 名字叫 "mbuf pool",一共有 NUM_MBUFS (4096) 个块
    // 这是为了避免频繁 malloc/free,提升性能
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS,0,0,RTE_MBUF_DEFAULT_BUF_SIZE,rte_socket_id());
    if(mbuf_pool == NULL){
    rte_exit(EXIT_FAILURE, "Could not create mbuf pool\\n");
    }

    // [Step 3] 调用上面的辅助函数,初始化端口
    ustack_init_port(mbuf_pool);

    // … 进入核心循环 …
    }

    4. 核心循环:收包、解析、释放(重点!)

    这是程序最繁忙的地方,也是最容易出错(比如内存泄漏)的地方。

    // [Step 4] 进入死循环,开始“干苦力” (收包-处理-释放)
    while(1){

    // 定义一个指针数组,用来存放收到的一批包
    struct rte_mbuf *mbufs[BURST_SIZE] = {0};

    // A. [核心收包 API]
    // 从 global_portid 的 0 号队列,尝试“抢”最多 BURST_SIZE (128) 个包
    // num_recvd 是实际抢到的包数量 (可能为 0,也可能为 128)
    uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);

    // 理论上不应该超过 BURST_SIZE
    if(num_recvd > BURST_SIZE){
    rte_exit(EXIT_FAILURE, "Received more packets than burst size\\n");
    }

    // B. 遍历处理抢到的每一个包
    int i = 0;
    for(i = 0; i < num_recvd; i++){

    // [零拷贝解析 – 第 1 层:以太网头]
    // rte_pktmbuf_mtod (Message to Data): 把 mbuf 转成以太网头结构体指针
    struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);

    // 过滤:检查以太网类型是否为 IPv4 (0x0800)
    // rte_cpu_to_be_16: 因为网络协议是大端序 (Big Endian),x86 CPU 是小端序,需要转换
    if(eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)){
    // 如果不是 IPv4 (比如是 ARP 或 IPv6),就不处理
    rte_pktmbuf_free(mbufs[i]); // 释放内存,把箱子还给池子
    continue; // 跳过,看下一个包
    }

    // [零拷贝解析 – 第 2 层:IP 头]
    // mtod_offset: 在开头的基础上,向后偏移 sizeof(ether_hdr) 长度,找到 IP 头
    struct rte_ipv4_hdr *iphdr = rte_pktmbuf_mtod_offset(mbufs[i], struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr));

    // 过滤:检查 IP 协议号是否为 UDP (17)
    if(iphdr->next_proto_id == IPPROTO_UDP){

    // [零拷贝解析 – 第 3 层:UDP 头]
    // 这里的指针加法 (iphdr + 1) 实际上是地址向后移动 sizeof(struct rte_ipv4_hdr)
    struct rte_udp_hdr *udphdr = (struct rte_udp_hdr *)(iphdr + 1);

    // [打印数据]
    // (char*)(udphdr + 1) 指向 UDP 头后面的数据部分 (Payload)
    // 注意:如果 Payload 不是以 \\0 结尾的字符串,这里可能会打印乱码
    printf("udp : %s\\n", (char*)(udphdr + 1));
    }

    // [释放内存]
    // 无论刚才是否打印了 UDP,处理完当前包后,必须手动归还内存
    // 如果忘了这行,4096 个箱子很快用完,程序就会无法再收包 (内存泄漏)
    rte_pktmbuf_free(mbufs[i]);
    }

    }

    完整可运行代码

    #include <stdio.h>
    #include <rte_eal.h>
    #include <rte_ethdev.h>
    #include <arpa/inet.h>

    int global_portid = 0;

    #define NUM_MBUFS 4096
    #define BURST_SIZE 128

    static const struct rte_eth_conf port_conf_default = {
    .rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
    };

    static int ustack_init_port(struct rte_mempool *mbuf_pool) {
    uint16_t nb_sys_ports = rte_eth_dev_count_avail();
    if (nb_sys_ports == 0) {
    rte_exit(EXIT_FAILURE, "No available ports\\n");
    }

    const int num_rx_queues = 1;
    const int num_tx_queues = 0;
    rte_eth_dev_configure(global_portid, num_rx_queues, num_tx_queues, &port_conf_default);

    if (rte_eth_rx_queue_setup(global_portid, 0, 128, rte_eth_dev_socket_id(global_portid), NULL, mbuf_pool) < 0) {
    rte_exit(EXIT_FAILURE, "Could not setup RX queue\\n");
    }

    if (rte_eth_dev_start(global_portid) < 0) {
    rte_exit(EXIT_FAILURE, "Could not start\\n");
    }

    return 0;
    }

    int main(int argc, char *argv[]) {
    if (rte_eal_init(argc, argv) < 0) {
    rte_exit(EXIT_FAILURE, "EAL initialization failed\\n");
    }

    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("mbuf pool", NUM_MBUFS, 0, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
    if (mbuf_pool == NULL) {
    rte_exit(EXIT_FAILURE, "Could not create mbuf pool\\n");
    }

    ustack_init_port(mbuf_pool);

    while (1) {
    struct rte_mbuf *mbufs[BURST_SIZE] = {0};

    uint16_t num_recvd = rte_eth_rx_burst(global_portid, 0, mbufs, BURST_SIZE);
    if (num_recvd > BURST_SIZE) {
    rte_exit(EXIT_FAILURE, "Received more packets than burst size\\n");
    }

    int i = 0;
    for (i = 0; i < num_recvd; i++) {
    struct rte_ether_hdr *eth_hdr = rte_pktmbuf_mtod(mbufs[i], struct rte_ether_hdr *);

    if (eth_hdr->ether_type != rte_cpu_to_be_16(RTE_ETHER_TYPE_IPV4)) {
    rte_pktmbuf_free(mbufs[i]);
    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);
    // 建议:实际项目中请使用 HexDump 打印二进制数据
    printf("udp payload : %s\\n", (char*)(udphdr + 1));
    }

    // 务必释放 mbuf
    rte_pktmbuf_free(mbufs[i]);
    }
    }

    printf("hello world\\n");
    return 0;
    }

    总结

    通过这个简单的 Demo,我们完成了从网卡抓包到解析 UDP 的全过程。虽然代码短,但它包含了 DPDK 最精髓的思想:EAL 初始化环境、Mempool 管理内存、Rx Burst 批量收包、Zero Copy 指针解析。

    0voice · GitHub

    赞(0)
    未经允许不得转载:171主机测评 » 【DPDK实战】编写一个高性能 UDP 抓包程序
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址