DPDK CPU Packet Processing - 4. IP分片与重组库
4.1 数据包分片(Packet fragmentation)
数据包分片例程(fragmentation routines)用于将输入的数据包划分成多个片段。
- 函数
rte_ipv4_fragment_packet()
和rte_ipv6_fragment_packet()
都假设:- 输入的
mbuf
的数据部分(data)指向的是 IP 头部的起始位置, - (即二层头部 L2 header 已经被去掉了)。
- 输入的
为了避免拷贝实际数据包的内容,
这些分片函数采用了零拷贝(zero-copy)技术,通过调用 rte_pktmbuf_attach()
实现。
在为每一个分片创建数据时,会生成两个新的 mbuf
:
- 直接 mbuf(Direct mbuf):
- 这个
mbuf
将包含新分片的数据包的 L3 头部(比如 IP 头)。
- 这个
- 间接 mbuf(Indirect mbuf):
- 这个
mbuf
是通过rte_pktmbuf_attach()
附着到原始数据包的。 - 它的数据指针指向原始数据包数据区中 加上分片偏移量的位置。
- 这个
接下来:
- 将原始
mbuf
中的 L3 头部复制到新创建的direct mbuf
中, - 并且更新 IP 头部,以反映新的分片状态。
需要注意的是:
- 对于 IPv4,不会重新计算头部校验和(header checksum),
- 校验和直接设置为 0。
最后:
- 每个分片对应的
direct mbuf
和indirect mbuf
, - 通过
mbuf
的 next 字段 链接在一起, - 从而组合成一个完整的分片数据包。
调用方(用户)可以明确指定用于分配:
direct mbuf
indirect mbuf
分别从哪个内存池(mempool)中分配。
这段内容主要说明了:
- 分片(fragmentation)不是简单地拷贝数据
- 而是尽量用 zero-copy 的方式减少 CPU、内存开销
在分片过程中:
名称 | 作用 | 特点 |
---|---|---|
Direct mbuf | 存放新的 L3/IP 头部信息 | 重新生成分片头部 |
Indirect mbuf | 指向原始 payload 的实际数据位置 | 复用原包数据,零拷贝 |
next pointer | 将 direct 和 indirect mbuf 串起来 | 形成一个完整的分片 |
原始数据包
┌──────────────────────────────────────┐
│ L2 Header | L3 Header | Payload │
└──────────────────────────────────────┘
分片后
→ 创建 Direct mbuf(复制 L3 Header)
→ 创建 Indirect mbuf(指向原始 Payload + 偏移)
→ 用 next 指针把 direct + indirect 连接成一个完整分片
→ 重复这个过程直到分完
4.2 数据包重组
4.2.1 IP 分片表(IP Fragment Table)
Fragment Table(分片表) 用于维护已经收到的数据包分片的信息。
- 每个 IP 包通过一个三元组唯一标识:
- <源 IP 地址>,<目的 IP 地址>,<IP 标识符 ID>。
需要注意:
- 对 Fragment Table 的所有更新和查找操作(update/lookup)
- 不是线程安全的(not thread-safe)。
- 因此,如果不同的执行上下文(线程/进程)要同时访问同一个分片表,
- 则必须由外部提供同步机制(external syncing mechanism)。
每个分片表条目可以保存一个数据包的分片信息,
最多可以保存 RTE_LIBRTE_IP_FRAG_MAX
个分片(默认值是 8 个)。
下面是一个创建新的 Fragment Table 的代码示例:
c
复制编辑
frag_cycles = (rte_get_tsc_hz() + MS_PER_S - 1) / MS_PER_S * max_flow_ttl;
bucket_num = max_flow_num + max_flow_num / 4;
frag_tbl = rte_ip_frag_table_create(max_flow_num, bucket_entries, max_flow_num, frag_cycles, socket_id);
内部实现细节:
- Fragment Table 实际上是一个简单的哈希表。
- 基本思路是:
- 使用两个哈希函数;
- 并为每个 key 提供 2 ×
<bucket_entries>
个可能的位置。
- 当发生哈希冲突且所有 2 ×
<bucket_entries>
都已被占用时:ip_frag_tbl_add()
函数不会重新插入已有 key 到其他位置,- 而是直接返回失败。
另外:
- 那些在表中存在超过
<max_cycles>
时间的条目, - 会被视为无效(invalid),
- 可以被新的条目移除或替换。
还需要特别注意:
- 数据包重组(reassembly)需要大量的 mbuf。
- 在任意时刻,Fragment Table 里可能最多存储:
plaintext
复制编辑
2 × bucket_entries × RTE_LIBRTE_IP_FRAG_MAX × <每个数据包最多的 mbuf 数量>
个等待剩余分片的数据包。
总结:
- 如何定位到一个数据包的所有分片?
- 通过三元组
<Src IP, Dst IP, ID>
,快速找到对应的重组记录。 - 记录保存在一个基于哈希表的 Fragment Table 中。
- 如何处理冲突与超时?
- 哈希冲突时,只允许尝试两次(primary + secondary bucket),如果还是冲突就失败。
- 超时机制(
max_cycles
)保证:- 如果一个分片包长时间没收齐,
- 系统可以自动清理掉对应的重组条目,防止内存泄漏。
4.2.2 数据包重组(Packet Reassembly)
分片数据包的处理和重组是通过以下函数完成的:
rte_ipv4_frag_reassemble_packet()
rte_ipv6_frag_reassemble_packet()
这些函数的返回值是:
- 成功时 → 返回一个指向有效
mbuf
的指针(即重组后的完整数据包); - 失败时 → 返回
NULL
(比如由于某些原因无法完成重组)。
这些重组函数的具体职责包括:
- 在 Fragment Table 中查找指定三元组
<IPv4源地址, IPv4目的地址, IP标识符ID>
的条目。 - 如果找到对应条目:
- 检查这个条目是否已经超时(timeout);
- 如果已经超时:
- 释放此前接收到的所有分片(free all previously received fragments);
- 从 Fragment Table 中移除相关信息。
- 如果没找到这样的条目:
- 试图通过以下两种方式创建一个新的条目:
- 使用一个空闲的 entry;
- 删除一个已经超时的 entry,释放其关联的 mbufs,并用新的 key 填充这个位置。
- 试图通过以下两种方式创建一个新的条目:
- 更新条目以记录新的分片信息,并检查该数据包是否已经可以完成重组(即该条目收到了所有分片)。
- 如果可以完成重组:
- 将所有分片重组为一个完整的包;
- 标记 Fragment Table 中对应的条目为空(empty);
- 返回重组后的
mbuf
给调用者。
- 如果还不能完成重组:
- 返回
NULL
给调用者(意味着还差一些分片未收到)。
- 返回
如果在数据包处理的任何阶段遇到错误,比如:
- 无法在 Fragment Table 中插入新的条目,
- 收到无效或超时的分片,
那么函数将:
- 释放与该数据包相关的所有分片(free all associated fragments);
- 标记对应的表条目为无效(invalid);
- 返回
NULL
给调用者。
可以总结成这个顺序:
步骤 | 动作 |
---|---|
1 | 查找 Fragment Table(通过 Src IP, Dst IP, ID) |
2 | 检查条目是否超时(timeout) |
3 | 如果超时 → 清理老分片 |
4 | 如果找不到 → 尝试新建 entry |
5 | 更新条目,添加新 fragment |
6 | 检查是否收齐所有分片 |
7 | - 如果收齐 → 重组、返回 mbuf |
8 | - 如果没收齐 → 返回 NULL |
9 | 如果中途遇到任何错误 → 清理所有关联分片,返回 NULL |