dpdk-4.接收+解析 ip tcp网络数据包

69 阅读13分钟

参考资料

dpdk第四课——接收网络数据包

前言

dpdk 分三个小组件:

  • core: cpu核心
  • queue:网口有多个队列
  • port:网口

队列:

  • 发送队列 RX
  • 接收队列 TX

初始化内存

struct rte_mempool *
rte_pktmbuf_pool_create(const char *name,        // 1. 内存池名称
                        unsigned n,             // 2. 池内 mbuf 总数
                        unsigned cache_size,    // 3. 每个核的缓存大小
                        uint16_t priv_size,     // 4. mbuf 私有数据大小
                        uint16_t data_room_size,// 5. mbuf 数据区大小
                        int socket_id           // 6. NUMA 节点 ID
);
  • 本质:创建一个 “缓冲区对象池”,池内存储的是预初始化的 rte_mbuf 结构体(含元数据区 + 数据区)。
  • 核心价值:rte_mbuf 从池内分配(rte_pktmbuf_alloc)、使用后归还(rte_pktmbuf_free),实现缓冲区复用,是 DPDK 高性能数据包处理的基础。
  • 关联逻辑:之前代码中,端口初始化(port_init_rx_only)时会将该内存池绑定到接收队列,网卡接收数据包时直接从池内获取 rte_mbuf 存储数据。

端口初始化

1. 定义端口基本配置

首先初始化端口配置结构体 struct rte_eth_conf,主要设置接收模式(rxmode)的关键参数:

struct rte_eth_conf port_conf = {
    .rxmode = {
        // 设置MTU(最大传输单元):以太网帧最大长度 - 以太网头 - CRC
        .mtu = RTE_ETHER_MAX_LEN - RTE_ETHER_HDR_LEN - RTE_ETHER_CRC_LEN,
    },
};
  • MTU 设置:确保端口能接收标准以太网帧(默认不超过 1500 字节,此处按 DPDK 宏计算标准值)。

2. 端口有效性检查

  • 检查端口是否有效:通过 rte_eth_dev_is_valid_port(port) 验证当前 port 是否为 DPDK 识别的有效端口(无效则返回错误)。

3. 获取网卡设备信息

调用 rte_eth_dev_info_get(port, &dev_info) 获取网卡硬件特性(如支持的最大队列数、描述符数量等),用于后续配置适配硬件能力。

4. 配置端口工作模式

调用 rte_eth_dev_configure(port, 1, 0, &port_conf) 配置端口核心参数:

  • 第二个参数 1:设置接收队列数量为 1(仅需一个接收队列用于抓包)。
  • 第三个参数 0:设置发送队列数量为 0(仅接收模式,无需发送功能)。
  • 第四个参数 &port_conf:传入前面定义的端口配置(如 MTU)。

5. 调整接收描述符数量

调用 rte_eth_dev_adjust_nb_rx_tx_desc(port, &nb_rxd, NULL) 调整接收队列的描述符数量:

  • 描述符是硬件用于管理数据包接收的 “指针”,数量需在网卡支持的范围内。
  • 此步骤确保 nb_rxd(初始为 RX_RING_SIZE 接收队列描述符数量)不超过网卡最大支持值,避免配置失败。

6. 初始化接收队列

调用 rte_eth_rx_queue_setup 为端口绑定接收队列,并关联内存池:

rte_eth_rx_queue_setup(port, 0, nb_rxd,
    rte_eth_dev_socket_id(port),  // 队列分配在网卡所在NUMA节点(提升性能)
    NULL, mbuf_pool);  // 关联全局内存池(接收的数据包用池中的mbuf存储)
  • 队列编号为 0(仅需一个接收队列)。
  • 关联 mbuf_pool:确保接收的数据包能从内存池申请缓冲区(避免内存泄漏,提升分配效率)。
rte_eth_rx_queue_setup

rte_eth_rx_queue_setup 作用是为指定端口的指定接收队列分配资源、绑定内存池,并配置队列参数,使网卡能将接收到的数据包正确存入内存。

rte_eth_rx_queue_setup(
    port,          // 端口号(要配置的网卡端口)
    0,             // 队列ID(本代码仅用0号接收队列)
    nb_rxd,        // 接收队列的描述符数量(已调整后的有效值)
    rte_eth_dev_socket_id(port),  // 队列所在的NUMA节点(提升性能)
    NULL,          // 队列的高级配置(如中断模式,默认NULL为轮询模式)
    mbuf_pool      // 关联的内存池(接收的数据包用该池中的mbuf存储)
);
rte_eth_dev_socket_id

rte_eth_dev_socket_id(port) 是一个 DPDK 辅助函数,用于获取指定网卡端口所在的 NUMA 节点 ID

  • 它的返回值是一个整数(如 0、1),代表网卡物理上连接的 NUMA 节点;
  • 作用是将软件资源(如接收队列、内存池)与网卡的 NUMA 节点绑定,减少跨节点内存访问的延迟(见下文 “NUMA 节点” 解释)。
NUMA

# NUMA
NUMA(Non-Uniform Memory Access,非统一内存访问)是现代多 CPU 服务器的硬件架构设计:

  • 系统被划分为多个 “NUMA 节点”,每个节点包含一组 CPU 核心和本地内存(节点内的 CPU 访问本地内存速度极快);
  • 跨节点访问内存(如节点 0 的 CPU 访问节点 1 的内存)会经过额外的总线,延迟显著增加(可能是本地访问的 2-3 倍)。

DPDK 的核心目标是 “高性能”,因此强烈建议将网卡、内存池、线程、队列等资源绑定到同一个 NUMA 节点,避免跨节点访问。例如:

  • 若网卡在 NUMA 节点 0,则内存池应分配在节点 0 的内存中,处理数据包的线程也应绑定到节点 0 的 CPU 核心。

7. 启动端口

调用 rte_eth_dev_start(port) 启动端口硬件,使其进入工作状态(开始监听网络并接收数据包)。

8. 获取并显示 MAC 地址

通过 rte_eth_macaddr_get(port, &addr) 获取端口 MAC 地址,并格式化输出(方便用户确认端口身份)。

9. 启用混杂模式

调用 rte_eth_promiscuous_enable(port) 开启端口混杂模式:

  • 默认情况下,网卡仅接收目的 MAC 匹配自身的数据包;混杂模式下,网卡会接收所有经过的数据包(无论目的 MAC 是否匹配),适合抓包场景。

补充:

混杂模式(Promiscuous Mode)

默认情况下,网卡会 “过滤” 数据包:只接收目的 MAC 地址与自身 MAC 匹配的数据包(单播)、广播包(目的 MAC 为FF:FF:FF:FF:FF:FF)或已加入的组播包。

混杂模式会关闭这种过滤,使网卡接收所有经过它的数据包(无论目的 MAC 是否匹配)。这是抓包工具(如 tcpdump、本代码)的核心需求 —— 需要捕获网络中的所有流量。

其他常见模式
  1. 单播模式(Unicast Mode) :默认模式,只接收目的 MAC 为自身的数据包。
  2. 广播模式(Broadcast Mode) :接收目的 MAC 为广播地址(FF:FF:FF:FF:FF:FF)的数据包(默认开启,无法关闭)。
  3. 组播模式(Multicast Mode) :只接收指定组播 MAC 地址的数据包(需通过 rte_eth_dev_mac_addr_add 加入组播组)。
  4. 多播过滤模式:可配置网卡只接收特定组播地址的数据包(减少无关流量)。

抓包 处理

1 RTE_ETH_FOREACH_DEV(port)

遍历所有可用端口

2 批量接收数据包 rte_eth_rx_burst

  • 声明 struct rte_mbuf *bufs[BURST_SIZE]:数组用于存储 “批量接收的数据包缓冲区”(BURST_SIZE=32,一次最多接收 32 个包)。
  • 调用 rte_eth_rx_burst(port, 0, bufs, BURST_SIZE):批量从指定端口的 0 号接收队列获取数据包。
    • 返回值 nb_rx:实际接收的数据包数量(0≤nb_rx≤BURST_SIZE)。
struct rte_mbuf
1. 核心定位
  • 所有网络数据包在 DPDK 中均以 rte_mbuf 形式流转(接收、解析、转发、发送)。
  • 内存池化管理:rte_mbuf 从 rte_mempool 中分配,使用后通过 rte_pktmbuf_free 归还,避免频繁内存分配 / 释放的开销。
  • 支持数据包分片(通过 next 指针串联分片)、头部扩展(预留 headroom)、数据追加(预留 tailroom)。
2. 关键设计理念
  • 元数据与数据分离rte_mbuf 分为两部分 —— 元数据区(存储数据包属性)和数据区(存储实际数据包内容)。
  • 无锁操作:元数据中的字段(如 pkt_len)通过原子操作或单线程访问保证线程安全,无需锁开销。
  • 对齐优化:数据区起始地址按硬件要求对齐(如 64B),提升 CPU 缓存命中率和网卡 DMA 传输效率。
struct rte_mbuf {
    // 1. 元数据区:描述数据包属性
    struct rte_mempool *pool;    // 所属内存池(释放时归还到此池)
    uint16_t data_off;           // 数据区起始地址相对于 mbuf 起始地址的偏移量(关键!)
    uint32_t pkt_len;            // 数据包总长度(所有头部 + 有效数据,用于统计)
    uint32_t buf_len;            // 数据区总容量(含 headroom + 数据 + tailroom)
    struct rte_mbuf *next;       // 分片链表指针(多分片数据包时串联下一个分片)
    
    // 2. 预留字段:硬件卸载、QoS 等功能使用(此处省略)
    // ...
    // 3. 数据区:存储实际数据包(通过 data_off 偏移定位)
    // 注:数据区不直接显式定义在结构体中,而是通过内存池分配时预留的空间,通过偏移访问
};
----------------------- struct rte_mbuf ---------------------------+
| 元数据区(pool、data_off、pkt_len、next 等) | 预留 headroom(头部扩展空间) |
+-----------------------------------------------------------------------+
|                          数据区(实际数据包内容)                      |
| (以太网头 → IPv4头 → TCP头 → 有效数据) | 预留 tailroom(数据追加空间) |
+-----------------------------------------------------------------------+
  • headroom(头部预留空间) :默认 128B,用于在解析过程中添加额外头部(如 VLAN 标签),避免移动数据。
  • tailroom(尾部预留空间) :默认 64B,用于追加数据(如加密后的 payload),无需重新分配缓冲区。
  • data_off 作用:指向数据区中 “实际数据包的起始位置”(即以太网头的起始地址),初始值等于 headroom 大小(如 128)。
rte_eth_rx_burst
rte_eth_rx_burst(
    port,    // 目标端口号(当前遍历到的端口)
    0,       // 接收队列ID(对应port_init_rx_only中配置的0号接收队列)
    bufs,    // 存储接收数据包的mbuf数组(输出参数)
    BURST_SIZE  // 最大接收数量(一次最多抓32个包)
);

3. 解析数据包

+--------------------- rte_mbuf 数据区 ---------------------+
| 0x00: 以太网头(struct rte_ether_hdr) → 14B              |
|  - dst_mac: 6B(目的MAC地址)                              |
|  - src_mac: 6B(源MAC地址)                               |
|  - ether_type: 2B(上层协议类型,如 0x0800=IPv4)          |
+----------------------------------------------------------+
| 0x0E: IPv4头(struct rte_ipv4_hdr) → 20B(无选项)       |
|  - version/IHL: 1B(版本+头部长度)                       |
|  - tos: 1B(服务类型)                                    |
|  - total_len: 2B(IPv4包总长度)                          |
|  - ...(其他字段省略)                                    |
|  - src_addr: 4B(源IP地址,大端序)                       |
|  - dst_addr: 4B(目的IP地址,大端序)                     |
|  - next_proto_id: 1B(上层协议,如 0x06=TCP)             |
+----------------------------------------------------------+
| 0x22: TCP头(struct rte_tcp_hdr) → 20B(无选项)         |
|  - src_port: 2B(源端口)                                 |
|  - dst_port: 2B(目的端口)                               |
|  - seq: 4B(序列号)                                      |
|  - ...(其他字段省略)                                    |
+----------------------------------------------------------+
| 0x36: 有效数据(payload) → 可变长度(如 HTTP 报文)       |
+----------------------------------------------------------+
| 尾部预留 tailroom(未使用)                               |
+----------------------------------------------------------+
  • 地址说明:0x00 是 rte_mbuf 数据区的起始地址(即 data_off 指向的位置),后续头部按固定长度偏移。
  • 解析逻辑:先从 0x00 解析以太网头,根据 ether_type 确定上层协议,再偏移 14B 解析 IPv4 头,以此类推。
3.1. 从 mbuf 中获取以太网头部
  • 核心操作:调用 rte_pktmbuf_mtod(pkt, struct rte_ether_hdr *),从数据包缓冲区(rte_mbuf)中提取以太网头部的起始地址。
    • 原理:rte_pktmbuf_mtod 是 DPDK 核心宏,意为 “memory to data”,直接返回 mbuf 中数据包的物理起始地址,跳过 mbuf 自身的元数据部分。
    • 目的:以太网头是数据包的最外层头部,必须先解析才能判断上层协议类型(如是否为 IPv4)。

#define rte_pktmbuf_mtod(m, t) rte_pktmbuf_mtod_offset(m, t, 0)

传入 struct rte_ether_hdr * 的含义:
pkt 对应的 rte_mbuf 数据区起始地址(data_off 指向的位置),存储的是一个 struct rte_ether_hdr 类型的以太网头。直接把头部size 类型转换成 传入类型。

struct rte_ether_hdr {
    struct rte_ether_addr dst_addr;  // 目的MAC地址(6B)
    struct rte_ether_addr src_addr;  // 源MAC地址(6B)
    uint16_t ether_type;             // 上层协议类型(2B)
} __rte_packed;  // 强制按1B对齐,避免编译器填充多余字节(关键!)
3.2. 解析以太网层信息
  • 3.2.1 提取并转换以太网类型(ether_type):
    • 调用 rte_be_to_cpu_16(eth_hdr->ether_type),将网络字节序(大端)的 ether_type 转换为主机字节序(小端)。
    • 原因:网络传输中数据默认使用大端字节序,而主机(如 x86 架构)是小端,不转换会导致数值读取错误(如 IPv4 的 0x0800 会读成 0x0008)。
    • 输出:打印 ether_type(如 0x0800=IPv4、0x0806=ARP、0x86DD=IPv6)。
  • 3.2.2 提取并格式化源 MAC 地址:
    • 声明 char buf[RTE_ETHER_ADDR_FMT_SIZE],用于存储格式化后的 MAC 地址字符串(DPDK 预定义宏,确保缓冲区足够)。
    • 调用 rte_ether_format_addr(buf, RTE_ETHER_ADDR_FMT_SIZE, &(eth_hdr->src_addr)),将二进制的源 MAC 地址(struct rte_ether_addr)格式化为 “xx:xx:xx:xx:xx:xx” 的字符串。
    • 输出:打印源 MAC 地址(如 “00:11:22:33:44:55”)。
3. 判定是否为 IPv4 协议,解析 IPv4 层信息

image.png

  • 条件判断:若 ether_type == RTE_ETHER_TYPE_IPV4(即以太网类型为 IPv4),则继续解析上层 IPv4 头部。
  • 3.1 获取 IPv4 头部:
    • 调用 rte_pktmbuf_mtod_offset(pkt, struct rte_ipv4_hdr *, sizeof(struct rte_ether_hdr)),从以太网头部后偏移指定长度(以太网头大小)的位置,提取 IPv4 头部地址。
    • 原理:rte_pktmbuf_mtod_offset 是带偏移量的 “内存到数据” 宏,适合多层头部解析(跳过外层头部,直接定位内层头部)。

rte_pktmbuf_mtod_offset,多了一个 offset 参数,用于叠加 “跳过外层头部的字节数”。

  • 3.2 提取并转换源 IP、目的 IP 地址:
    • 调用 rte_be_to_cpu_32(ipv4_hdr->src_addr) 和 rte_be_to_cpu_32(ipv4_hdr->dst_addr),将 32 位大端序的 IP 地址转换为主机字节序。
    • 格式化输出:通过位运算(>>24&0xFF)将 32 位整数拆分为 4 个字节,以 “点分十进制” 格式打印(如 “192.168.1.1 → 10.0.0.1”)。
  • 3.3 提取上层协议类型(protocol):
    • 读取 ipv4_hdr->next_proto_id 字段,该字段标识 IPv4 上层的传输层协议。
    • 协议类型(如 0x06=TCP、0x17=UDP、0x01=ICMP)。
4. 解析 TCP

image.png

  • 【端口号】:端口号,用于多路复用或者分解来自(送到)上层的数据;
  • 【序列号】:在连接建立时由计算机计算出的初始值,通过 SYN 包传给对端主机,每发送一次新的数据包,就累加一次该序列号的大小。用来解决网络包乱序问题
  • 【确认应答号】:指下次期望收到的数据的序列号,发送端收到这个确认应答以后可以确认确认应答号-1的数据包已经被正常接收。主要用来解决不丢包的问
  • 【标志字段】:
    • 【ACK】:用以指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收的报文段的确认;
    • 【RST】:用以指示连接的强制拆除,当接收到错误连接时会发送RST位置为1的报文;
    • 【SYN】:用以指示连接的建立,该位为1的报文表示希望建立连接;
    • 【FIN】:用以指示连接的终止,该位为1的报文表示希望断开连接;