参考资料
前言
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、本代码)的核心需求 —— 需要捕获网络中的所有流量。
其他常见模式
- 单播模式(Unicast Mode) :默认模式,只接收目的 MAC 为自身的数据包。
- 广播模式(Broadcast Mode) :接收目的 MAC 为广播地址(
FF:FF:FF:FF:FF:FF)的数据包(默认开启,无法关闭)。 - 组播模式(Multicast Mode) :只接收指定组播 MAC 地址的数据包(需通过
rte_eth_dev_mac_addr_add加入组播组)。 - 多播过滤模式:可配置网卡只接收特定组播地址的数据包(减少无关流量)。
抓包 处理
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 层信息
- 条件判断:若
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
- 【端口号】:端口号,用于多路复用或者分解来自(送到)上层的数据;
- 【序列号】:在连接建立时由计算机计算出的初始值,通过 SYN 包传给对端主机,每发送一次新的数据包,就累加一次该序列号的大小。用来解决网络包乱序问题;
- 【确认应答号】:指下次期望收到的数据的序列号,发送端收到这个确认应答以后可以确认
确认应答号-1的数据包已经被正常接收。主要用来解决不丢包的问; - 【标志字段】:
- 【ACK】:用以指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收的报文段的确认;
- 【RST】:用以指示连接的强制拆除,当接收到错误连接时会发送RST位置为1的报文;
- 【SYN】:用以指示连接的建立,该位为1的报文表示希望建立连接;
- 【FIN】:用以指示连接的终止,该位为1的报文表示希望断开连接;