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 mbufindirect mbuf
  • 通过 mbufnext 字段 链接在一起,
  • 从而组合成一个完整的分片数据包

调用方(用户)可以明确指定用于分配:

  • 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 数量>

个等待剩余分片的数据包。

总结:

  1. 如何定位到一个数据包的所有分片?
  • 通过三元组 <Src IP, Dst IP, ID>,快速找到对应的重组记录。
  • 记录保存在一个基于哈希表的 Fragment Table 中。
  1. 如何处理冲突与超时?
  • 哈希冲突时,只允许尝试两次(primary + secondary bucket),如果还是冲突就失败。
  • 超时机制(max_cycles)保证:
    • 如果一个分片包长时间没收齐,
    • 系统可以自动清理掉对应的重组条目,防止内存泄漏。
4.2.2 数据包重组(Packet Reassembly)

分片数据包的处理和重组是通过以下函数完成的:

  • rte_ipv4_frag_reassemble_packet()
  • rte_ipv6_frag_reassemble_packet()

这些函数的返回值是:

  • 成功时 → 返回一个指向有效 mbuf 的指针(即重组后的完整数据包);
  • 失败时 → 返回 NULL(比如由于某些原因无法完成重组)。

这些重组函数的具体职责包括:

  1. 在 Fragment Table 中查找指定三元组 <IPv4源地址, IPv4目的地址, IP标识符ID> 的条目。
  2. 如果找到对应条目:
    • 检查这个条目是否已经超时(timeout);
    • 如果已经超时:
      • 释放此前接收到的所有分片(free all previously received fragments);
      • 从 Fragment Table 中移除相关信息。
  3. 如果没找到这样的条目:
    • 试图通过以下两种方式创建一个新的条目:
      • 使用一个空闲的 entry;
      • 删除一个已经超时的 entry,释放其关联的 mbufs,并用新的 key 填充这个位置。
  4. 更新条目以记录新的分片信息,并检查该数据包是否已经可以完成重组(即该条目收到了所有分片)。
  5. 如果可以完成重组:
    • 将所有分片重组为一个完整的包;
    • 标记 Fragment Table 中对应的条目为空(empty);
    • 返回重组后的 mbuf 给调用者。
  6. 如果还不能完成重组:
    • 返回 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
posted @ 2025-04-27 22:36  Tohomson  阅读(120)  评论(0)    收藏  举报