BPF Verifier bpf_patch_insn_data 当前实现分析
本文最后更新于:2026年1月22日 早上
BPF Verifier bpf_patch_insn_data 当前实现分析
概述
本文档详细分析 Linux 内核主线代码中 bpf_patch_insn_data() 函数的当前实现,解释为什么它成为 BPF 验证器的主要性能瓶颈(占用约 47% 的验证时间)。
BPF 程序的完整生命周期
在理解为什么需要修补指令之前,先了解 BPF 程序从编写到执行的完整流程:
1 | |
关键阶段说明:
编译阶段(用户空间):
- 用户使用 C 语言编写 BPF 程序
- Clang/LLVM 编译为 BPF 字节码(类似汇编)
- 生成 ELF 格式的 .o 文件
加载阶段(用户空间 → 内核):
- 用户空间工具(bpftool/libbpf)解析 .o 文件
- 通过
bpf(BPF_PROG_LOAD, ...)系统调用传递给内核 - 内核接收原始的 BPF 字节码
验证阶段(内核空间):
- Verifier 检查程序的安全性
- 确保不会崩溃内核或访问非法内存
- 这个阶段不修改指令,只是分析
修补阶段(内核空间)⭐ 性能瓶颈所在:
- 将抽象的 BPF 指令转换为可执行的形式
- 插入安全检查
- 优化和重写指令
- 这个阶段会调用 bpf_patch_insn_data() 数千次
JIT 编译阶段(内核空间,可选):
- 将 BPF 字节码编译为本机机器码(x86_64/ARM64 等)
- 提高运行时性能
执行阶段(内核空间):
- BPF 程序附加到内核 hook 点(如网络包处理、系统调用跟踪等)
- 事件触发时执行 BPF 程序
为什么修补阶段是瓶颈?
- 对于简单程序:验证 + 修补可能只需要几毫秒
- 对于复杂程序(如 pyperf180):
- 验证阶段:约 250ms
- 修补阶段:约 220ms(占 47%) ⭐
- JIT 编译:约 10ms
修补阶段占用了近一半的加载时间,这就是为什么优化 bpf_patch_insn_data() 如此重要。
为什么 BPF Verifier 需要修补指令?
在上述流程的修补阶段,verifier 需要对验证通过的 BPF 字节码进行转换和增强。修补(patch)指令的主要原因包括:
上下文访问转换(Context Access Conversion)
- 用户编写的 BPF 程序使用抽象的上下文结构(如
struct __sk_buff) - 内核需要将这些抽象访问转换为实际的内核结构访问
- 一个简单的字段访问可能需要展开成多条指令
具体例子:访问网络包数据指针
1
2
3
4
5// 用户编写的 BPF 代码
int bpf_prog(struct __sk_buff *skb) {
void *data = (void *)(long)skb->data; // 抽象访问
// ...
}编译后的 BPF 字节码(简化):
1
r1 = *(u32 *)(r1 + offsetof(__sk_buff, data)) // 1 条指令Verifier 修补后的实际指令:
1
2
3
4
5// 需要从实际的 struct sk_buff 中加载
r2 = *(u64 *)(r1 + offsetof(sk_buff, head)) // 加载 head 指针
r3 = *(u16 *)(r1 + offsetof(sk_buff, data)) // 加载 data 偏移
r3 <<= 0 // 零扩展
r1 = r2 + r3 // 计算实际地址1 条抽象指令 → 4 条实际指令,需要调用
bpf_patch_insn_data()将 1 条指令替换为 4 条。类似的转换还包括:
skb->len→ 从sk_buff->len加载skb->protocol→ 从sk_buff->protocol加载并转换字节序skb->mark→ 从sk_buff->mark加载
一个典型的网络 BPF 程序可能有几十个这样的字段访问,每个都需要修补。
- 用户编写的 BPF 程序使用抽象的上下文结构(如
Helper 函数调用修复(Helper Call Fixup)
- BPF 程序调用 helper 函数时使用的是函数 ID
- verifier 需要将函数 ID 转换为实际的函数地址
- 可能需要插入额外的参数检查和转换指令
- 某些 helper 调用需要内联展开
具体例子:调用
bpf_map_lookup_elem()1
2
3
4
5
6
7
8// 用户编写的 BPF 代码
struct bpf_map_def my_map = { ... };
int bpf_prog(struct __sk_buff *skb) {
int key = 0;
void *value = bpf_map_lookup_elem(&my_map, &key);
// ...
}编译后的 BPF 字节码:
1
2
3r1 = map_fd // map 文件描述符
r2 = fp - 4 // key 的栈地址
call BPF_FUNC_map_lookup_elem // helper 函数 IDVerifier 修补后:
1
2
3
4
5
6
7
8
9
10
11// 1. 将 map_fd 转换为内核 map 指针
r1 = <map_ptr> // 直接使用内核指针
// 2. 参数已经准备好
r2 = fp - 4
// 3. 将函数 ID 替换为实际函数地址
call <bpf_map_lookup_elem_addr> // 实际函数地址
// 4. 可能插入返回值检查
if r0 == 0 goto error // 空指针检查(某些情况)某些 helper 函数还需要更复杂的转换:
bpf_probe_read()→ 可能内联为多条内存访问指令bpf_get_current_task()→ 直接访问 per-CPU 变量bpf_ktime_get_ns()→ 可能内联为 RDTSC 指令(x86_64)
安全检查插入(Security Instrumentation)
- 插入边界检查指令(防止越界访问)
- 插入空指针检查
- 插入除零检查
- 插入栈溢出检查
具体例子:数组访问边界检查
1
2
3
4
5
6
7// 用户编写的 BPF 代码
int bpf_prog(struct __sk_buff *skb) {
char buf[64];
int idx = skb->len % 64;
buf[idx] = 1; // 数组访问
// ...
}编译后的 BPF 字节码:
1
2
3
4
5r1 = *(u32 *)(r1 + offsetof(__sk_buff, len))
r1 &= 63 // idx = len % 64
r2 = fp - 64 // buf 的地址
r2 += r1 // buf + idx
*(u8 *)(r2 + 0) = 1 // buf[idx] = 1Verifier 修补后(插入边界检查):
1
2
3
4
5
6
7
8
9
10
11
12
13
14r1 = *(u32 *)(r1 + offsetof(__sk_buff, len))
r1 &= 63
// ⭐ 插入边界检查(verifier 添加)
if r1 >= 64 goto error // 确保 idx < 64
r2 = fp - 64
r2 += r1
// ⭐ 插入栈访问检查(verifier 添加)
if r2 < fp - 512 goto error // 确保在栈范围内
if r2 >= fp goto error
*(u8 *)(r2 + 0) = 1原本 5 条指令 → 修补后 9 条指令
其他安全检查例子:
- 除零检查:
r1 = r2 / r3→ 插入if r3 == 0 goto error - 空指针检查:
r1 = *(u64 *)r2→ 插入if r2 == 0 goto error - 指针运算检查:确保指针运算不会溢出
优化和重写(Optimization and Rewriting)
- 常量折叠后的指令替换
- 死代码消除
- 跳转优化
- 尾调用优化
具体例子:常量折叠和指令优化
1
2
3
4
5
6
7
8// 用户编写的 BPF 代码
int bpf_prog(struct __sk_buff *skb) {
int x = 10 + 20; // 常量表达式
if (x > 25) { // 可以在编译时确定
return 1;
}
return 0;
}编译后的 BPF 字节码:
1
2
3
4
5
6
7
8r0 = 10
r0 += 20 // r0 = 30
if r0 > 25 goto label_1 // 总是跳转
r0 = 0
exit
label_1:
r0 = 1
exitVerifier 优化后:
1
2
3
4// 常量折叠 + 死代码消除
r0 = 1 // 直接返回 1
exit
// 删除了不可达的代码原本 6 条指令 → 优化后 2 条指令
其他优化例子:
- 跳转链优化:
goto A; A: goto B→goto B - 无用跳转消除:
goto next_insn→ 删除 - ALU 指令简化:
r1 = r1 + 0→ 删除 - 尾调用优化:将函数调用转换为跳转
架构特定转换(Architecture-Specific Conversion)
- 某些指令在特定架构上需要不同的实现
- 32 位 vs 64 位的差异处理
- 字节序转换
具体例子:64 位立即数加载
BPF 指令集中,加载 64 位立即数需要特殊处理:
1
2// 用户编写的 BPF 代码
void *ptr = (void *)0x123456789abcdef0ULL; // 64 位地址编译后的 BPF 字节码(BPF_LD_IMM64):
1
2// BPF_LD_IMM64 是一个特殊的双字指令
r1 = 0x123456789abcdef0 // 占用 2 个指令槽在某些架构上,verifier 可能需要修补为:
1
2
3
4
5
6
7// x86_64: 可以直接使用
r1 = 0x123456789abcdef0
// 32 位架构: 需要分两次加载
r1 = 0x9abcdef0 // 低 32 位
r1 <<= 32
r1 |= 0x12345678 // 高 32 位其他架构特定转换:
字节序转换:网络字节序 ↔ 主机字节序
1
2
3// 用户代码: ntohs(skb->protocol)
// 大端架构: 不需要转换
// 小端架构: 需要插入 bswap 指令原子操作:不同架构的原子指令实现不同
1
2
3// BPF 原子加法
// x86_64: lock xadd
// ARM64: ldxr/stxr 循环除法指令:某些架构没有硬件除法
1
2
3// r1 = r2 / r3
// 有硬件除法: 直接使用 div 指令
// 无硬件除法: 调用软件除法函数
修补的频率:
- 简单程序:几十次修补
- 复杂程序(如 pyperf180):数千甚至数万次修补
- 每次修补可能将 1 条指令展开成多条指令
为什么修补是性能瓶颈:
- 修补操作非常频繁(M 很大)
- 每次修补都需要移动整个数组(O(N))
- 累积效应:O(N*M) 的时间复杂度
关键发现
- 当前实现使用数组结构存储 BPF 指令
- 每次修补都需要移动整个数组
- 时间复杂度为 O(N*M),其中 N 是指令数,M 是修补次数
- 对于复杂程序(如 pyperf180),占用 46.99% 的验证时间
当前实现方案
核心流程
位置:kernel/bpf/verifier.c:21304
1 | |
调用统计
在 verifier.c 中,bpf_patch_insn_data 被调用 32 次,主要在以下场景:
- convert_ctx_accesses() - 上下文访问转换
- fixup_bpf_calls() - BPF 调用修复
- do_misc_fixups() - 其他修复
核心函数详解
1. bpf_patch_insn_single() - 主要瓶颈(占 27.72%)
位置:kernel/bpf/core.c:451
为什么是瓶颈?核心问题是数组插入操作的 O(N) 复杂度。
问题根源:数组中间插入的代价
BPF 程序使用连续数组存储指令:
1 | |
关键问题:
- 每次修补都要移动修补点之后的所有指令
- 对于 10,000 条指令的程序,在位置 1,000 修补,需要移动 9,000 条指令
- 如果修补 5,000 次,累积移动次数达到数千万次
代码实现分析
1 | |
性能开销详细分析
假设程序有 N = 10,000 条指令,在位置 off = 3,000 修补,插入 len = 3 条指令:
| 操作 | 复杂度 | 实际操作数 | 占比 | 说明 |
|---|---|---|---|---|
| memmove 指令 | O(N - off) | 7,000 条指令 | ~60% | 移动修补点后的所有指令 |
| bpf_adj_branches | O(N) | 10,000 条指令 | ~25% | 遍历所有指令调整跳转 |
| bpf_prog_realloc | O(N) | 10,000 条指令 | ~10% | 可能触发内存复制 |
| memcpy 新指令 | O(len) | 3 条指令 | <1% | 复制新指令 |
| bpf_adj_linfo | O(L) | L 个行信息 | ~5% | 调整行信息 |
总时间复杂度:O(N)
累积效应:为什么占 27.72% 的时间?
对于复杂程序(如 pyperf180),假设:
- 程序长度:N = 10,000 条指令
- 修补次数:M = 5,000 次
- 平均修补位置:off_avg = N / 2 = 5,000
每次修补的平均开销:
1 | |
实际测试数据验证(pyperf180.bpf.o):
1 | |
为什么 memmove 这么慢?
内存带宽限制:
- 每条 BPF 指令 = 8 字节
- 移动 5,000 条指令 = 40 KB
- 5,000 次修补 × 40 KB = 200 MB 数据移动
- 即使内存带宽 10 GB/s,也需要 20 ms
缓存失效:
- 大块内存移动导致 CPU 缓存失效
- 每次 memmove 都要重新加载数据到缓存
- 缓存未命中的代价很高
非对齐访问:
- memmove 可能涉及非对齐内存访问
- 在某些架构上性能更差
为什么 bpf_adj_branches 也很慢?
1 | |
为什么慢:
- 必须遍历所有指令(O(N))
- 每次修补都要遍历一次
- M 次修补 = M × N 次指令检查
- 5,000 × 10,000 = 50,000,000 次检查
瓶颈总结
bpf_patch_insn_single 占 27.72% 的原因:
memmove 指令数组(~60% 的时间):
- 每次修补移动数千条指令
- 累积移动数千万次
- 内存带宽和缓存失效
bpf_adj_branches(~25% 的时间):
- 每次修补遍历整个程序
- 累积遍历数千万次
- 分支预测失败
bpf_prog_realloc(~10% 的时间):
- 频繁的内存重新分配
- 可能触发内存复制
累积效应:
- 单次修补不慢(几微秒)
- 但修补次数太多(数千次)
- O(N) × M 次 = O(N×M) 总复杂度
核心问题:数组结构不适合频繁的中间插入操作!
1 | |
性能开销:
memmove:O(N) - 移动修补点后的所有指令bpf_adj_branches:O(N) - 遍历所有指令调整跳转bpf_prog_realloc:O(N) - 可能触发内存复制
2. adjust_insn_aux_data() - 第三大瓶颈
位置:kernel/bpf/verifier.c:21221
1 | |
性能开销:
memmove:O(N) - 移动修补点后的所有辅助数据- 循环初始化:O(cnt) - 通常 cnt 较小
3. 其他 adjust 函数
1 | |
性能瓶颈分析
1. 时间复杂度分析
假设一个程序有 N 条指令,需要进行 M 次修补:
1 | |
对于复杂程序(如 pyperf180):
- N ≈ 10,000 条指令
- M ≈ 5,000 次修补
- 总操作数 ≈ 50,000,000 次指令移动
2. 每次修补的操作开销
| 操作 | 复杂度 | 占比 | 说明 |
|---|---|---|---|
| memmove 指令数组 | O(N) | 27.72% | 移动修补点后的所有指令 |
| bpf_adj_branches | O(N) | ~10% | 遍历所有指令调整跳转 |
| memmove aux_data | O(N) | 13.43% | 移动修补点后的所有辅助数据 |
| vrealloc aux_data | O(N) | 5.84% | 重新分配并复制(len > 1 时) |
| bpf_prog_realloc | O(N) | ~5% | 重新分配程序内存 |
| adjust_subprog_starts | O(S) | <1% | S = 子程序数量 |
| adjust_insn_arrays | O(A) | <1% | A = 指令数组映射数量 |
| adjust_poke_descs | O(P) | <1% | P = poke 描述符数量 |
总复杂度:O(N) 每次修补
3. 实际性能数据(Baseline 测试)
测试程序:pyperf180.bpf.o
内核版本:6.19.0-rc5
验证时间:0.4766 ± 0.0150 秒
perf 热点分析:
1 | |
关键发现:
bpf_patch_insn_data占用近 47% 的验证时间- 这证实了 Eduard Zingerman 的分析(他说约 40%)
- 这是最大的性能瓶颈
4. 历史性能数据(Jiong Wang 2019)
极端大规模修补场景的测试数据:
| 修补次数 | 当前耗时 | 理想耗时 | 提升比例 |
|---|---|---|---|
| 15,000 | 3秒 | <0.5秒 | 83% |
| 45,000 | 29秒 | <1秒 | 97% |
| 95,000 | 125秒 | <2秒 | 98% |
| 195,000 | 712秒 | <5秒 | 99% |
| 1,000,000 | 5100秒 (1.4小时) | <30秒 | 99%+ |
为什么这么设计?
历史原因
BPF verifier 最初设计时:
- 程序规模较小(几百条指令)
- 修补次数不多(几十次)
- 简单的数组实现足够用
数组实现的优势:
- 简单直观,易于理解
- 随机访问 O(1)
- 内存连续,缓存友好
现在的问题
现代 BPF 程序越来越复杂:
- 指令数可达数万条
- 修补次数可达数千甚至数万次
- O(N*M) 复杂度变得无法接受
数组实现的劣势:
- 插入/删除操作 O(N)
- 频繁的内存移动
- 无法批量处理
具体例子:为什么这么慢?
场景:10,000 条指令的程序,需要修补 5,000 次
当前实现(数组)
1 | |
理想实现(链表)
1 | |
为什么占 47% 的验证时间?
原因分析
修补是最频繁的操作:
- convert_ctx_accesses:每个上下文访问都可能修补
- fixup_bpf_calls:每个 helper 调用都可能修补
- do_misc_fixups:各种优化和修复
每次修补都是 O(N) 操作:
- 移动指令数组
- 调整跳转偏移
- 移动辅助数据
累积效应:
- 对于复杂程序,M 很大
- O(N*M) 的累积效应非常明显
- 最终占据近一半的验证时间
与其他操作的对比
1 | |
关键观察:
bpf_patch_insn_data单个函数占用 47%do_check包含多个子函数,总共占用 46%- 优化
bpf_patch_insn_data的收益最明显
优化方向
当前实现的核心问题
- ✅ 简单直观:数组实现,易于理解
- ❌ O(N*M) 复杂度:每次修补都要移动整个数组
- ❌ 频繁内存操作:大量 memmove 和 realloc
- ❌ 重复遍历:每次都要调整跳转、子程序等
- ❌ 无法批量处理:一次只能修补一个位置
可能的优化方案
方案 1:批量修补 API
核心思路:将多个修补操作收集起来,排序后一次性处理,减少 memmove 次数。
当前问题:
1 | |
批量修补方案:
1 | |
API 设计:
1 | |
实现步骤:
1 | |
为什么从后往前修补?
关键原因:保持前面位置的偏移不变!
1 | |
性能提升分析:
假设 5,000 次修补操作:
| 操作 | 当前实现 | 批量修补 | 提升 |
|---|---|---|---|
| memmove 次数 | 5,000 次 | 1 次(或少量) | 99% |
| bpf_adj_branches | 5,000 次 | 1 次 | 99% |
| 内存分配 | 5,000 次 | 1 次 | 99% |
但实际提升只有 10-15%,为什么?
不是所有修补都能批量化:
- 某些修补依赖前面修补的结果
- 只能批量化同一阶段的修补(如 convert_ctx_accesses)
批量化的开销:
- 需要排序(O(M log M))
- 需要额外的内存存储批次
- 需要更复杂的偏移计算
实际可批量化的比例:
- 假设只有 30% 的修补可以批量化
- 5,000 次修补 → 1,500 次可批量,3,500 次仍然单独
- 提升:1,500 / 5,000 = 30% 的修补操作被优化
- 整体提升:30% × 47% = 14%
优势:
- 改动相对较小,不需要重构核心数据结构
- 可以逐步实施(先优化一个阶段,再扩展到其他阶段)
- 风险可控,容易测试
劣势:
- 提升有限(10-15%)
- 不能解决根本问题(数组结构)
- 实现仍然有一定复杂度
方案 2:链表 + 延迟线性化
核心思路:使用链表存储指令,修补时只需链表插入(O(1)),最后统一线性化为数组。
当前问题:数组插入 = O(N),M 次修补 = O(N×M)
链表方案:链表插入 = O(1),M 次修补 = O(M),最后线性化 = O(N),总计 = O(M + N)
方案 3:Gap Buffer(间隙缓冲区)
核心思路:在数组中维护一个”间隙”(gap),修补操作在间隙处进行,只需移动间隙而不是整个数组。
数据结构:
1 | |
工作原理:
1 | |
性能分析:
- 单次修补:O(|new_pos - gap_pos|) - 移动间隙的代价
- 如果修补位置集中:接近 O(1)
- 如果修补位置分散:退化为 O(N)
- 平均情况:O(N/2) 每次修补
优势:
- 实现相对简单(比链表简单)
- 内存连续,缓存友好
- 如果修补位置有局部性,性能很好
劣势:
- 最坏情况仍是 O(N×M)
- 需要预分配额外空间(间隙)
- 不如链表彻底
适用场景:
- 修补位置有局部性(如连续修补)
- 不想引入链表的复杂性
- 作为过渡方案
方案 4:Rope(绳索)数据结构
核心思路:使用平衡树(如 B-tree)存储指令块,每个节点存储一小段连续指令。
数据结构:
1 | |
工作原理:
1 | |
性能分析:
- 单次修补:O(log N) - 树的高度
- M 次修补:O(M log N)
- 线性化:O(N) - 遍历树
优势:
- 理论上最优:O(M log N + N)
- 支持高效的随机访问
- 可以支持更多操作(如删除、查询)
劣势:
- 实现非常复杂(需要平衡树算法)
- 内存开销大(树节点 + 指针)
- 缓存不友好(指针跳转)
- 过度设计(对 BPF verifier 来说)
适用场景:
- 需要频繁随机访问
- 需要支持复杂操作
- 性能要求极高
方案 5:分块数组(Chunked Array)
核心思路:将指令分成固定大小的块,修补时只需移动块内的指令。
数据结构:
1 | |
工作原理:
1 | |
性能分析:
- 单次修补:O(CHUNK_SIZE) - 最多移动一个块
- M 次修补:O(M × CHUNK_SIZE)
- 如果 CHUNK_SIZE = 256,比 O(N) 快很多
优势:
- 实现简单(比链表简单)
- 性能可预测
- 内存局部性好(块内连续)
- 可以调整 CHUNK_SIZE 平衡性能和内存
劣势:
- 不如链表彻底(仍需移动)
- 需要选择合适的 CHUNK_SIZE
- 块之间需要链表连接
适用场景:
- 折中方案(性能 vs 复杂度)
- 不想引入完整链表
- 作为渐进式优化
方案 6:Copy-on-Write(写时复制)+ 版本链
核心思路:每次修补创建新版本,使用版本链记录变化,最后合并。
数据结构:
1 | |
工作原理:
1 | |
性能分析:
- 单次修补:O(1) - 只记录 delta
- M 次修补:O(M)
- 合并:O(N + M) - 应用所有 delta
优势:
- 修补阶段极快(O(1))
- 可以回滚(保留历史版本)
- 可以批量优化 delta
劣势:
- 合并阶段复杂(需要处理重叠)
- 内存开销大(保存所有 delta)
- 实现复杂度中等
适用场景:
- 需要支持回滚
- 修补次数非常多
- 可以延迟合并
数据结构对比总结
| 方案 | 时间复杂度 | 空间复杂度 | 实现复杂度 | 缓存友好 | 推荐度 |
|---|---|---|---|---|---|
| 当前数组 | O(N×M) | O(N) | 简单 | ✅ 很好 | ❌ 太慢 |
| 链表 | O(M+N) | O(N) | 中等 | ❌ 差 | ⭐⭐⭐⭐⭐ 最推荐 |
| Gap Buffer | O(N×M/2) | O(N+gap) | 简单 | ✅ 好 | ⭐⭐⭐ 折中 |
| Rope | O(M log N + N) | O(N + 树开销) | 很复杂 | ❌ 差 | ⭐⭐ 过度设计 |
| 分块数组 | O(M×CHUNK) | O(N) | 简单 | ✅ 较好 | ⭐⭐⭐⭐ 不错 |
| COW + 版本链 | O(M+N) | O(N+M) | 中等 | ✅ 好 | ⭐⭐⭐ 有潜力 |
详细分析:为什么链表仍是最佳选择?
1. Gap Buffer 的问题
理论:如果修补位置集中,性能接近 O(1)
现实:BPF verifier 的修补位置分散
convert_ctx_accesses():遍历整个程序fixup_bpf_calls():遍历整个程序do_misc_fixups():遍历整个程序
结果:间隙需要频繁移动,退化为 O(N×M)
示例:
1 | |
2. Rope 的问题
理论:O(M log N) 很优秀
现实:
- 实现复杂度太高(需要 AVL 树或红黑树)
- 内存开销大(每个节点需要指针、高度等元数据)
- 缓存不友好(树节点分散在内存中)
- 对于 BPF verifier,O(M log N) vs O(M) 差异不大
计算:
- N = 10,000, M = 5,000
- Rope: O(5,000 × log 10,000) ≈ O(5,000 × 13) = 65,000
- 链表: O(5,000 + 10,000) = 15,000
- 链表仍然快 4 倍,且实现简单得多
3. 分块数组的问题
理论:O(M × CHUNK_SIZE) 比 O(N×M) 好
现实:
- 仍然需要移动(只是少一些)
- 需要选择合适的 CHUNK_SIZE(太小:块太多,太大:移动太多)
- 块之间需要链表连接(引入链表复杂度)
计算:
- CHUNK_SIZE = 256
- 平均每次修补移动 128 条指令
- M = 5,000 次修补
- 总移动:5,000 × 128 = 640,000 条指令
- 链表:0 次移动(只有指针操作)
结论:分块数组是折中方案,但不如链表彻底
4. COW + 版本链的问题
理论:修补阶段 O(1),合并阶段 O(M+N)
现实:
- 合并阶段需要处理重叠的 delta(复杂)
- 内存开销大(保存所有 delta)
- 不需要回滚功能(BPF verifier 不需要)
示例:重叠 delta 处理
1 | |
结论:复杂度不值得,链表更简单
混合方案:链表 + 分块优化
核心思路:结合链表和分块的优势
数据结构:
1 | |
优势:
- 链表的 O(1) 插入
- 分块的缓存友好性
- 减少链表节点数(每个节点存多条指令)
劣势:
- 实现更复杂
- 块内仍需移动(如果块满了)
- 收益有限(链表已经够快)
结论:过度优化,不推荐
最终推荐:链表方案
理由:
彻底解决问题:
- O(M+N) vs O(N×M)
- 理论最优(除了 Rope,但 Rope 太复杂)
实现复杂度可接受:
- 比 Rope 简单得多
- 比 COW 简单
- 比分块数组略复杂,但收益大得多
社区认可:
- 2019 年 Jiong Wang RFC
- 2019 年 Andrii Nakryiko 建议
- 历史失败是工程问题,不是方案问题
性能提升显著:
- 30-40% 验证时间减少
- 其他方案提升有限
无需额外功能:
- 不需要回滚(COW)
- 不需要复杂查询(Rope)
- 只需要高效插入和最后线性化
其他方案的定位:
- Gap Buffer:如果不想引入链表,可以作为过渡方案(10-15% 提升)
- 分块数组:同上,折中方案
- Rope:过度设计,不推荐
- COW:不需要回滚功能,不推荐
行动建议:
- 首选:直接实施链表方案(18 个补丁)
- 备选:如果担心风险,先实施 Gap Buffer 或分块数组(2-3 个补丁),但要明确这只是过渡方案
- 不推荐:Rope 和 COW(复杂度不值得)
数据结构设计(基于 Andrii Nakryiko 2019 年建议):
1 | |
工作流程:
1 | |
性能对比:
| 操作 | 当前实现(数组) | 链表方案 | 提升 |
|---|---|---|---|
| 单次修补 | O(N) memmove | O(1) 链表插入 | N 倍 |
| M 次修补 | O(N×M) | O(M) | N 倍 |
| 跳转调整 | M 次,每次 O(N) | 1 次,O(N) | M 倍 |
| 线性化 | 不需要 | 1 次,O(N) | 新增开销 |
| 总复杂度 | O(N×M) | O(M + N) | 显著 |
实际性能提升(pyperf180,N=10,000, M=5,000):
- 当前:O(10,000 × 5,000) = 50,000,000 操作
- 链表:O(5,000 + 10,000) = 15,000 操作
- 理论提升:3,333 倍
- 实际提升:30-40%(因为还有其他开销,如 do_check 占 46%)
为什么 2019 年 Jiong Wang 的链表方案未合入?
维护者担心风险(Alexei Starovoitov):
“tbh sounds scary. We had so many bugs in the patch_insn over years.”
- patch_insn 历史上有很多 bug
- 大规模重构容易引入新 bug
- 影响所有 BPF 程序,后果严重
实现复杂度高:
- line info 调整:Jiong 在 RFC 中承认实现困难
“line info I need some careful implementation and I failed to have clean code for this during linearization”
- RFC 版本中 line info 处理不完善
- 线性化阶段的 line info 调整代码不够干净
- subprog info 调整:需要处理子程序被删除的情况
- RFC 中这部分实现也不完整
- 子程序边界调整的边界情况复杂
- 需要处理各种边界情况
- 代码审查困难,RFC 代码质量不够高
- line info 调整:Jiong 在 RFC 中承认实现困难
测试覆盖困难:
- 需要大量测试用例
- 需要伪随机测试验证等价性
- 难以保证所有场景都正确
方案分歧:
- Jiong 提出链表方案
- Alexei 提出更复杂的基本块方案
- 讨论转向更彻底的重构,但更复杂
缺乏持续推动:
- Jiong 发布 RFC 后没有继续迭代
- 没有 v2/v3 版本
- 社区没有其他人接手
优先级不够:
- 虽然有性能问题,但不是致命的
- 大多数程序验证时间可接受
- 有更紧急的功能需要实现
如果要实现链表方案,需要:
- 充分的测试覆盖(包括伪随机测试)
- 渐进式实施(先实现核心,再扩展)
- 与维护者充分沟通,获得支持
- 准备好迭代多个版本
- 证明与现有实现等价
优势:
- 彻底解决性能问题(30-40% 提升)
- 理论上最优的方案
- 有社区讨论基础(2019 年 RFC)
劣势:
- 实现复杂,风险高
- 需要大量测试
- 可能需要数月时间
- 历史上未成功的先例
方案 3:基本块(Basic Block)方案(Alexei 2022 提出)
- 目标:使用编译器技术,将程序分割为基本块
- 预期提升:可能更高,但实现复杂
- 风险:非常高
- 状态:仅讨论,未实现
什么是基本块(Basic Block)?
基本块是编译器理论中的概念,指的是一段顺序执行的指令序列,具有以下特点:
- 单一入口:只能从第一条指令进入
- 单一出口:只能从最后一条指令退出
- 顺序执行:中间没有跳转指令(除了最后一条)
基本块示例:
1 | |
Alexei 提出的基本块方案思路:
分割阶段:将 BPF 程序分割为基本块
1
原始程序 → [BB1, BB2, BB3, ..., BBn]跳转转换:将相对偏移转换为基本块索引
1
2原始:if r3 > 30 goto +3 (相对偏移)
转换:if r3 > 30 goto BB3 (基本块索引)修补阶段:在基本块内部修补,无需调整跳转
1
2
3
4修补 BB2 内的指令:
- 只需要移动 BB2 内的指令
- 不需要调整其他基本块的跳转
- 因为跳转目标是基本块索引,不是偏移重建阶段:从基本块重建完整程序
1
2
3[BB1, BB2, BB3, ...] → 最终程序
- 计算每个基本块的起始位置
- 将基本块索引转换回相对偏移
基本块方案的优势:
修补局部化:
- 修补只影响单个基本块
- 不需要移动其他基本块的指令
- 减少 memmove 的范围
跳转调整延迟:
- 修补时不需要调整跳转
- 只在最后重建时统一调整
- 避免重复遍历
更好的缓存局部性:
- 基本块通常较小
- 修补操作更集中
基本块方案的劣势:
实现极其复杂:
- 需要实现基本块分析算法
- 需要维护基本块图(Control Flow Graph)
- 需要处理各种边界情况
数据结构复杂:
- 需要维护基本块数组
- 需要维护跳转映射表
- 需要处理基本块的分裂和合并
调试困难:
- 中间表示更抽象
- 错误更难定位
- 测试覆盖困难
风险极高:
- 影响 verifier 核心逻辑
- 容易引入难以发现的 bug
- 维护成本高
为什么基本块方案未实现?
- 过度设计:对于 BPF verifier 的需求来说太复杂
- 风险太高:Alexei 自己都说 “sounds scary”
- 投入产出比:实现成本远高于预期收益
- 有更简单的方案:链表方案已经足够好
对比:链表方案 vs 基本块方案
| 特性 | 链表方案 | 基本块方案 |
|---|---|---|
| 实现复杂度 | 中等 | 极高 |
| 性能提升 | 30-40% | 可能 40-50% |
| 风险 | 高 | 极高 |
| 维护成本 | 中等 | 很高 |
| 社区接受度 | 有先例(Jiong Wang RFC) | 仅讨论 |
| 推荐度 | 可考虑 | 不推荐 |
参考资料
2019年讨论 - Jiong Wang 的链表方案
https://lore.kernel.org/bpf/CAEf4BzYDAVUgajz4=dRTu5xQDddp5pi2s=T1BdFmRLZjOwGypQ@mail.gmail.com/2022年讨论 - Eduard Zingerman 的优化建议
https://lore.kernel.org/bpf/CAEf4BzY_E8MSL4mD0UPuuiDcbJhh9e2xQo2=5w+ppRWWiYSGvQ@mail.gmail.com/2026年反馈 - Eduard 指出 bpf_patch_insn_data 占用 40% 时间
https://www.spinics.net/lists/kernel/msg6011549.htmlBaseline 测试结果
linux/BASELINE_RESULTS.txt历史 Patch 分析
linux/HISTORICAL_PATCHES_STATUS.txtlinux/WHY_NOT_MERGED.txt
总结
当前 bpf_patch_insn_data 实现:
- 使用数组存储指令
- 每次修补都移动整个数组
- 时间复杂度 O(N*M)
- 占用 47% 的验证时间
性能瓶颈根源:
- memmove 指令数组:27.72%
- memmove aux_data:13.43%
- vrealloc:5.84%
优化潜力:
- 批量修补:10-15% 提升
- 链表重构:30-40% 提升(对复杂程序)
历史教训:
- 2019 年 Jiong Wang 的链表方案未合入(风险太高,缺乏持续推动)
- 2022 年 Alexei 提出基本块方案未实现(过度复杂,风险极高)
- 基本块方案虽然理论上更优,但实现成本远超收益
- 需要采用渐进式、低风险的优化策略
推荐的优化路径
基于以上分析和历史教训,必须面对一个现实:
核心问题:数组结构根本不适合频繁的中间插入操作
- 批量修补 API 只是减少 memmove 次数,但每次仍是 O(N)
- 无论如何优化,只要使用数组,就绕不开读写和扩容的性能瓶颈
- 链表方案是唯一能彻底解决问题的方案
方案对比:治标 vs 治本
| 特性 | 批量修补 API | 链表方案 |
|---|---|---|
| 本质 | 治标:减少操作次数 | 治本:改变数据结构 |
| memmove | 仍需要,只是次数少 | 完全避免 |
| 单次修补 | O(N) | O(1) |
| M 次修补 | O(N×M/k),k=批次数 | O(M) |
| 数组读写 | 仍然频繁 | 只在最后线性化 |
| 数组扩容 | 仍然需要 | 只在最后一次 |
| 性能提升 | 10-15%(治标) | 30-40%(治本) |
| 根本问题 | 未解决 | 已解决 |
结论:批量修补只是权宜之计,链表方案才是正确方向。
推荐方案:直接实施链表方案
为什么必须选择链表方案?
这是唯一的根本解决方案:
- 数组结构的性能瓶颈无法通过小修小补解决
- 批量修补只是延缓问题,不能解决问题
- 链表是唯一能将 O(N×M) 降低到 O(M+N) 的方案
历史失败的原因可以克服:
- Jiong Wang RFC 失败的主要原因:
- ❌ line info 实现不完善 → ✅ 可以重新设计
- ❌ subprog info 处理不完整 → ✅ 可以完善
- ❌ 缺乏持续推动 → ✅ 我们可以持续投入
- ❌ 测试覆盖不足 → ✅ 可以补充测试
- 这些都是工程问题,不是方案本身的问题
- Jiong Wang RFC 失败的主要原因:
社区已有讨论基础:
- 2019 年 Jiong Wang RFC
- 2019 年 Andrii Nakryiko 的改进建议
- 2022 年 Eduard Zingerman 的讨论
- 2026 年 Eduard 再次建议优化
- 社区认可这个方向,只是缺少完善的实现
投入产出比最高:
- 批量修补:2 个月,10-15% 提升,但问题仍在
- 链表方案:6 个月,30-40% 提升,彻底解决
- 长期看,链表方案的投入产出比更高
实施策略:渐进式链表方案
不要重蹈 Jiong Wang 的覆辙,采用渐进式实施:
阶段 1:核心链表实现(2 个月)
目标:实现基本的链表修补功能,暂不处理复杂场景
1 | |
成功标准:
- 简单程序验证正确
- 性能提升 > 20%
- 为下一阶段打好基础
阶段 2:完善元数据处理(2 个月)
目标:解决 Jiong Wang RFC 中的问题
1 | |
成功标准:
- 所有元数据正确处理
- 所有 selftests 通过
- 无功能回归
阶段 3:测试和优化(1-2 个月)
目标:充分测试,确保可以合入
1 | |
成功标准:
- 通过所有测试
- 性能提升 30-40%
- 代码质量高,易于审查
阶段 4:社区提交和迭代(1-2 个月)
1 | |
关键成功因素
解决 RFC 中的具体问题:
- line info:必须有干净的实现
- subprog info:必须处理所有边界情况
- 不能留下未完成的部分
充分的测试覆盖:
- 功能测试:所有 selftests
- 等价性测试:伪随机测试
- 压力测试:大规模程序
- 性能测试:详细的 perf 数据
与维护者充分沟通:
- 提前发 RFC 征求意见
- 展示清晰的设计和收益
- 及时响应审查意见
- 准备好长期投入
渐进式实施:
- 不要一次性提交所有代码
- 先提交核心功能
- 再逐步完善
- 每一步都充分测试
风险和应对
风险 1:实现比预期复杂
- 应对:渐进式实施,每个阶段都可以独立评估
- 如果某个阶段遇到困难,可以调整计划
风险 2:维护者不接受
- 应对:提前沟通,展示清晰的收益数据
- 强调这是唯一的根本解决方案
- 参考历史讨论,说明如何解决之前的问题
风险 3:引入新的 bug
- 应对:充分的测试覆盖
- 伪随机测试验证等价性
- 渐进式合入,每一步都验证
风险 4:时间投入过长
- 应对:6 个月的时间投入是值得的
- 彻底解决问题,长期收益大
- 可以分阶段评估,及时调整
为什么不推荐批量修补 API?
虽然批量修补风险更低,但:
治标不治本:
- 仍然使用数组,性能瓶颈仍在
- 只是减少操作次数,不能改变 O(N) 的本质
- 10-15% 的提升不够显著
浪费时间:
- 花 2 个月实现批量修补
- 发现效果有限,还是要做链表
- 总共花 8 个月,不如直接花 6 个月做链表
代码债务:
- 批量修补的代码最终会被链表方案替换
- 维护两套代码增加复杂度
- 不如直接做正确的事情
总结:必须选择链表方案
核心观点:
- 数组结构的性能瓶颈无法通过小修小补解决
- 链表方案是唯一能彻底解决问题的方案
- 历史失败的原因是工程问题,不是方案问题
- 采用渐进式实施,可以降低风险
行动计划:
- 直接实施链表方案(6 个月)
- 渐进式实施,分 4 个阶段
- 充分测试,确保质量
- 与社区保持沟通
预期结果:
- 彻底解决 bpf_patch_insn_data 的性能瓶颈
- 验证时间减少 30-40%
- 为 BPF 社区做出重要贡献
- 建立技术信誉
核心原则:
- 做正确的事情,而不是简单的事情
- 治本,而不是治标
- 长期投入,而不是短期收益
- 渐进式实施,降低风险
- 充分测试,确保质量
文档版本:v1.0
日期:2026-01-22
作者:Qiliang Yuan realwujing@gmail.com
测试环境:Linux 6.19.0-rc5
Patch Series 规划(18 个补丁)- 复用现有函数名
设计原则
复用现有函数名,修改实现:
- ✅ 保持 API 兼容性
- ✅ 减少代码变更
- ✅ 更容易审查
- ✅ 更容易理解
可复用的现有函数:
adjust_insn_aux_data()- 调整 aux_dataadjust_subprog_starts()- 调整子程序adjust_insn_arrays()- 调整指令数组adjust_poke_descs()- 调整 poke 描述符bpf_patch_insn_data()- 主入口函数
总览
| 阶段 | 补丁数 | 周期 | 目标 |
|---|---|---|---|
| 阶段 1:核心实现 | 8 | 1-8 周 | 链表基础功能 + RFC |
| 阶段 2:元数据处理 | 6 | 9-16 周 | 完整功能 + V1 |
| 阶段 3:优化测试 | 4 | 17-24 周 | 性能优化 + 合入 |
阶段 1:核心链表实现(Patch 1-8)
1. bpf: introduce patchable instruction data structures
文件:include/linux/bpf_verifier.h
内容:定义链表节点和修补器结构(新增)
1 | |
2. bpf: add patcher initialization and cleanup helpers
文件:kernel/bpf/verifier.c
内容:新增辅助函数(不复用,因为是新概念)
1 | |
说明:这些是链表特有的概念,当前代码中不存在对应函数,必须新增。
3. bpf: implement linked list instruction patching
文件:kernel/bpf/verifier.c
内容:新增链表修补函数
1 | |
说明:这是链表修补的核心函数,实现 O(1) 的插入操作,当前代码中不存在对应函数。
4. bpf: implement program linearization
文件:kernel/bpf/verifier.c
内容:新增线性化函数
1 | |
说明:将链表转换回数组,这是链表方案特有的步骤,当前代码中不存在对应函数。
5. bpf: implement jump offset adjustment for linked list
文件:kernel/bpf/verifier.c
内容:新增跳转调整函数
1 | |
说明:链表版本的跳转调整,使用 orig_idx → new_idx 映射,与数组版本的 bpf_adj_branches() 逻辑不同,必须新增。
6. bpf: add new bpf_patch_insn_data_list() API
文件:kernel/bpf/verifier.c
内容:实现完整的链表版本主函数,移除前面函数的 __maybe_unused
1 | |
注意:
- 此时移除 Patch 2-5 中函数的
__maybe_unused(因为已被使用) - 这个函数本身仍标记
__maybe_unused(Patch 7 才使用)
7. bpf: add CONFIG_BPF_VERIFIER_LINKED_LIST option
文件:kernel/bpf/Kconfig, kernel/bpf/verifier.c
内容:添加编译选项,复用 bpf_patch_insn_data() 函数名
1 | |
关键:
- 复用
bpf_patch_insn_data()函数名 - 通过
#ifdef切换实现 - 移除
bpf_patch_insn_data_list()的__maybe_unused
8. selftests/bpf: add basic tests
文件:tools/testing/selftests/bpf/prog_tests/verifier_patch_list.c
内容:基础功能测试
阶段 2:元数据处理(Patch 9-14)
9. bpf: refactor adjust_insn_aux_data for linked list
文件:kernel/bpf/verifier.c
内容:复用 adjust_insn_aux_data() 函数名,修改实现
1 | |
关键:
- 复用
adjust_insn_aux_data()函数名 - 修改函数实现(链表版本)
- 保持函数签名不变
10. bpf: add line info adjustment for linked list
文件:kernel/bpf/verifier.c
内容:新增 line info 调整(当前代码中 line info 调整在 bpf_adj_linfo() 中,但在 kernel/bpf/core.c)
1 | |
说明:当前代码中 bpf_adj_linfo() 在 kernel/bpf/core.c 中,我们需要在 verifier.c 中新增链表版本。
11. bpf: refactor adjust_subprog_starts for linked list
文件:kernel/bpf/verifier.c
内容:复用 adjust_subprog_starts() 函数名
1 | |
12. bpf: refactor adjust_poke_descs for linked list
文件:kernel/bpf/verifier.c
内容:复用 adjust_poke_descs() 函数名
1 | |
13. bpf: refactor adjust_insn_arrays for linked list
文件:kernel/bpf/verifier.c
内容:复用 adjust_insn_arrays() 函数名
1 | |
14. selftests/bpf: add comprehensive tests
文件:tools/testing/selftests/bpf/prog_tests/verifier_patch_list.c
内容:测试所有元数据场景
阶段 3:优化和测试(Patch 15-18)
15. selftests/bpf: add equivalence test
文件:tools/testing/selftests/bpf/prog_tests/verifier_patch_equiv.c
内容:伪随机测试,验证等价性
16. selftests/bpf: add stress test
文件:tools/testing/selftests/bpf/prog_tests/verifier_patch_stress.c
内容:大规模测试
17. bpf: optimize memory allocation
文件:kernel/bpf/verifier.c
内容:内存池优化
18. bpf: remove array-based patching implementation
文件:kernel/bpf/Kconfig, kernel/bpf/verifier.c, kernel/bpf/core.c
内容:
- 删除
CONFIG_BPF_VERIFIER_LINKED_LIST选项 - 删除所有
#ifdef/#else分支 - 只保留链表实现
- 删除旧代码(约 300 行):
bpf_patch_insn_single()及相关函数- 数组版本的
adjust_*函数
- 保留所有函数名:
bpf_patch_insn_data()- 链表实现adjust_insn_aux_data()- 链表实现adjust_subprog_starts()- 链表实现adjust_insn_arrays()- 链表实现adjust_poke_descs()- 链表实现
最终代码:
1 | |
复用函数名总结
| 函数名 | 当前存在 | 操作 | 说明 |
|---|---|---|---|
bpf_patch_insn_data() |
✅ | 复用 | 主入口,通过 #ifdef 切换实现 |
adjust_insn_aux_data() |
✅ | 复用 | 修改实现,使用 orig_idx → new_idx 映射 |
adjust_subprog_starts() |
✅ | 复用 | 修改实现,使用 orig_idx → new_idx 映射 |
adjust_insn_arrays() |
✅ | 复用 | 修改实现,使用 orig_idx → new_idx 映射 |
adjust_poke_descs() |
✅ | 复用 | 修改实现,使用 orig_idx → new_idx 映射 |
alloc_patchable_insn() |
❌ | 新增 | 链表节点分配(链表特有概念) |
init_patcher() |
❌ | 新增 | 初始化 patcher(数组 → 链表) |
free_patcher() |
❌ | 新增 | 清理 patcher |
patch_insn_list() |
❌ | 新增 | 链表修补(O(1) 插入) |
linearize_patcher() |
❌ | 新增 | 线性化(链表 → 数组) |
adjust_branches_list() |
❌ | 新增 | 跳转调整(链表版本) |
adjust_line_info_list() |
❌ | 新增 | line info 调整(链表版本) |
bpf_patch_insn_data_list() |
❌ | 临时 | 临时函数名,Patch 18 删除 |
复用策略:
- ✅ 5 个核心函数复用:保持 API 兼容性,只修改内部实现
- ❌ 7 个新增函数:链表特有概念,必须新增
- 🔄 1 个临时函数:
bpf_patch_insn_data_list()在 Patch 18 会被删除
优势:
- ✅ 最大化复用现有函数名(5/13 = 38%)
- ✅ 保持 API 兼容性
- ✅ 更容易审查(函数名熟悉)
- ✅ 更容易理解(概念一致)
- ✅ 减少代码变更量
为什么不能复用更多?
链表特有概念:
init_patcher()/free_patcher()- 数组方案不需要patch_insn_list()- 链表插入逻辑完全不同linearize_patcher()- 数组方案不需要线性化
实现逻辑差异:
adjust_branches_list()- 使用 orig_idx → new_idx 映射,与bpf_adj_branches()逻辑完全不同adjust_line_info_list()- 链表版本的 line info 调整
临时过渡:
bpf_patch_insn_data_list()- 在 Patch 7-17 期间作为#ifdef分支,Patch 18 删除
最终状态(Patch 18 后):
- 只保留 5 个复用的函数名 + 7 个新增函数
- 删除
bpf_patch_insn_data_list()和所有旧数组实现 - 总计 12 个函数,其中 5 个是复用的函数名
Bisectability 保证
关键技术:
- Patch 1-6:使用
__maybe_unused标记 - Patch 7-17:新代码在
#ifdef CONFIG_BPF_VERIFIER_LINKED_LIST内 - Patch 18:删除
#ifdef和旧代码
编译验证:每个补丁测试两种配置
CONFIG_BPF_VERIFIER_LINKED_LIST=n(默认)CONFIG_BPF_VERIFIER_LINKED_LIST=y(测试)
详细实现指南
Patch 1: 数据结构定义
修改文件:include/linux/bpf_verifier.h
添加内容:
1 | |
编译验证:
1 | |
Patch 2-6: 核心函数实现
详细实现代码见上文 Patch 2-6 的描述。每个 patch 添加后都要验证编译通过。
Patch 7: CONFIG 选项
详细实现见上文 Patch 7 的描述。关键是通过 #ifdef 切换实现,默认使用旧实现。
Patch 9-13: 元数据处理
每个 patch 都在 #ifdef CONFIG_BPF_VERIFIER_LINKED_LIST 内添加新实现,保持旧实现不变。
Patch 18: 删除旧实现
删除所有 #ifdef 分支和旧代码,只保留链表实现。详细见上文描述。
实现检查清单
Patch 1/18: bpf: introduce patchable instruction data structures
文件修改:
-
include/linux/bpf_verifier.h- 添加struct bpf_patchable_insn -
include/linux/bpf_verifier.h- 添加struct bpf_patcher
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无编译警告
- 无编译错误
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 代码注释完整
- 数据结构对齐正确
Commit Message:
- Subject 简洁明了(<50 字符)
- Body 解释为什么需要这些数据结构
- 包含
Signed-off-by: Qiliang Yuan <realwujing@gmail.com>
Patch 2/18: bpf: add patcher initialization and cleanup helpers
文件修改:
-
kernel/bpf/verifier.c- 添加alloc_patchable_insn() -
kernel/bpf/verifier.c- 添加init_patcher() -
kernel/bpf/verifier.c- 添加free_patcher()
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无 “unused function” 警告(使用
__maybe_unused) - 内存分配使用正确的 GFP 标志
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 错误处理完整(内存分配失败)
- 资源清理正确(free_patcher)
- 函数注释完整
功能验证:
-
init_patcher()正确将数组转换为链表 -
orig_to_node映射正确建立 -
free_patcher()无内存泄漏
Commit Message:
- Subject: “bpf: add patcher initialization and cleanup helpers”
- Body 解释数组 → 链表转换的必要性
- 说明使用
__maybe_unused的原因 - 签名完整
Patch 3/18: bpf: implement linked list instruction patching
文件修改:
-
kernel/bpf/verifier.c- 添加patch_insn_list()
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 边界检查完整(off 有效性)
- 错误处理完整(内存分配失败)
- 函数注释说明 O(1) 复杂度
功能验证:
-
len == 1时正确替换指令 -
len > 1时正确插入多条指令 - 链表指针正确维护
-
total_cnt正确更新
Commit Message:
- Subject: “bpf: implement linked list instruction patching”
- Body 强调 O(1) 插入 vs O(N) memmove
- 签名完整
Patch 4/18: bpf: implement program linearization
文件修改:
-
kernel/bpf/verifier.c- 添加linearize_patcher()
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 内存分配使用
bpf_prog_realloc() - 错误处理完整
- 函数注释说明线性化过程
功能验证:
-
new_idx正确分配(从 0 开始递增) - 指令正确复制到新数组
- 程序长度正确(
new_prog->len == patcher->total_cnt)
Commit Message:
- Subject: “bpf: implement program linearization”
- Body 解释链表 → 数组转换
- 签名完整
Patch 5/18: bpf: implement jump offset adjustment for linked list
文件修改:
-
kernel/bpf/verifier.c- 添加adjust_branches_list()
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 跳转类型判断正确(JMP/JMP32)
- 边界检查完整
- 函数注释说明使用 orig_idx → new_idx 映射
功能验证:
- 条件跳转偏移正确调整
- 无条件跳转偏移正确调整
- 新增指令(orig_idx == -1)正确跳过
- 跳转目标越界检查
Commit Message:
- Subject: “bpf: implement jump offset adjustment for linked list”
- Body 解释为什么只需调整一次(vs 每次修补都调整)
- 签名完整
Patch 6/18: bpf: add new bpf_patch_insn_data_list() API
文件修改:
-
kernel/bpf/verifier.c- 添加bpf_patch_insn_data_list() -
kernel/bpf/verifier.c- 移除 Patch 2-5 函数的__maybe_unused
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 错误处理完整(每个步骤)
- 资源清理正确(失败时释放 patcher 和 new_prog)
- 函数注释说明完整流程
功能验证:
- 初始化成功
- 修补成功
- 线性化成功
- 跳转调整成功
- 错误路径正确清理资源
Commit Message:
- Subject: “bpf: add new bpf_patch_insn_data_list() API”
- Body 说明这是链表方案的主入口
- 说明仍标记
__maybe_unused(Patch 7 才使用) - 签名完整
Patch 7/18: bpf: add CONFIG_BPF_VERIFIER_LINKED_LIST option
文件修改:
-
kernel/bpf/Kconfig- 添加CONFIG_BPF_VERIFIER_LINKED_LIST -
kernel/bpf/verifier.c- 修改bpf_patch_insn_data()添加#ifdef -
kernel/bpf/verifier.c- 移除bpf_patch_insn_data_list()的__maybe_unused
编译验证:
-
make defconfig && ./scripts/config -d BPF_VERIFIER_LINKED_LIST && make kernel/bpf/verifier.o- 编译通过(默认配置) -
./scripts/config -e BPF_VERIFIER_LINKED_LIST && make kernel/bpf/verifier.o- 编译通过(启用链表) - 两种配置都无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - Kconfig 帮助文本清晰
-
#ifdef分支清晰 - 默认配置保持旧行为(
default n)
功能验证:
-
CONFIG=n时使用数组实现 -
CONFIG=y时使用链表实现 - 两种配置都能正常工作
Commit Message:
- Subject: “bpf: add CONFIG_BPF_VERIFIER_LINKED_LIST option”
- Body 说明这是实验性功能,默认关闭
- 说明如何测试两种实现
- 签名完整
Patch 8/18: selftests/bpf: add basic tests
文件修改:
-
tools/testing/selftests/bpf/config- 添加CONFIG_BPF_VERIFIER_LINKED_LIST=y -
tools/testing/selftests/bpf/prog_tests/verifier_patch_list.c- 新增测试
编译验证:
-
make -C tools/testing/selftests/bpf- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 测试用例覆盖基本场景
- 测试代码有
#ifdef保护
功能验证:
- 简单程序验证通过
- 单条指令替换测试
- 多条指令插入测试
- 跳转调整测试
- 测试通过率 100%
Commit Message:
- Subject: “selftests/bpf: add basic tests for linked list patching”
- Body 说明测试覆盖范围
- 签名完整
Patch 9/18: bpf: adjust insn_aux_data for linked list patching
文件修改:
-
kernel/bpf/verifier.c- 在#ifdef内添加adjust_insn_aux_data_list() -
kernel/bpf/verifier.c- 修改bpf_patch_insn_data_list()调用新函数
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 使用 orig_idx → new_idx 映射
- 错误处理完整
- 函数注释说明与数组版本的区别
功能验证:
-
aux_data正确调整 -
seen标志正确传播 -
zext_dst正确设置
Commit Message:
- Subject: “bpf: adjust insn_aux_data for linked list patching”
- Body 说明如何使用映射调整 aux_data
- 签名完整
Patch 10/18: bpf: add line info adjustment for linked list
文件修改:
-
kernel/bpf/verifier.c- 在#ifdef内添加adjust_line_info_list() -
kernel/bpf/verifier.c- 修改bpf_patch_insn_data_list()调用新函数
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 使用 orig_idx → new_idx 映射
- 错误处理完整
- 函数注释说明 line info 调整逻辑
功能验证:
- Line info 正确调整
- 行号映射正确
- 调试信息完整
Commit Message:
- Subject: “bpf: add line info adjustment for linked list”
- Body 说明这解决了 Jiong Wang RFC 中的问题
- 签名完整
Patch 11/18: bpf: refactor adjust_subprog_starts for linked list
文件修改:
-
kernel/bpf/verifier.c- 在#ifdef内添加链表版本 -
kernel/bpf/verifier.c- 保持#else分支的旧实现
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 使用 orig_idx → new_idx 映射
- 处理子程序被删除的情况
- 函数注释说明边界情况
功能验证:
- 子程序起始位置正确调整
- 子程序边界正确
- 多子程序场景正确
Commit Message:
- Subject: “bpf: refactor adjust_subprog_starts for linked list”
- Body 说明如何处理子程序边界
- 签名完整
Patch 12/18: bpf: refactor adjust_poke_descs for linked list
文件修改:
-
kernel/bpf/verifier.c- 在#ifdef内添加链表版本 -
kernel/bpf/verifier.c- 保持#else分支的旧实现
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 使用 orig_idx → new_idx 映射
- 错误处理完整
功能验证:
- Poke 描述符正确调整
- 尾调用场景正确
Commit Message:
- Subject: “bpf: refactor adjust_poke_descs for linked list”
- Body 说明 poke 描述符的作用
- 签名完整
Patch 13/18: bpf: refactor adjust_insn_arrays for linked list
文件修改:
-
kernel/bpf/verifier.c- 在#ifdef内添加链表版本 -
kernel/bpf/verifier.c- 保持#else分支的旧实现
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 使用 orig_idx → new_idx 映射
- 错误处理完整
功能验证:
- 指令数组映射正确调整
- 多个数组场景正确
Commit Message:
- Subject: “bpf: refactor adjust_insn_arrays for linked list”
- Body 说明指令数组映射的作用
- 签名完整
Patch 14/18: selftests/bpf: add comprehensive tests
文件修改:
-
tools/testing/selftests/bpf/prog_tests/verifier_patch_list.c- 扩展测试
编译验证:
-
make -C tools/testing/selftests/bpf- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 测试覆盖所有元数据场景
功能验证:
- aux_data 测试通过
- line info 测试通过
- subprog 测试通过
- poke 描述符测试通过
- insn_arrays 测试通过
- 所有 selftests 通过
Commit Message:
- Subject: “selftests/bpf: add comprehensive tests for metadata”
- Body 列出测试覆盖的场景
- 签名完整
Patch 15/18: selftests/bpf: add equivalence test
文件修改:
-
tools/testing/selftests/bpf/prog_tests/verifier_patch_equiv.c- 新增伪随机测试
编译验证:
-
make -C tools/testing/selftests/bpf- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 伪随机测试框架完整
- 测试可重现(固定种子)
功能验证:
- 数组实现 vs 链表实现结果一致
- 测试覆盖各种修补场景
- 测试覆盖各种程序大小
- 1000+ 次随机测试通过
Commit Message:
- Subject: “selftests/bpf: add equivalence test for array vs linked list”
- Body 说明伪随机测试的重要性
- 签名完整
Patch 16/18: selftests/bpf: add stress test
文件修改:
-
tools/testing/selftests/bpf/prog_tests/verifier_patch_stress.c- 新增压力测试
编译验证:
-
make -C tools/testing/selftests/bpf- 编译通过 - 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 压力测试场景完整
功能验证:
- 大规模程序测试(10 万条指令)
- 极端修补场景(10 万次修补)
- 内存使用测试(无泄漏)
- 性能测试(验证提升)
Commit Message:
- Subject: “selftests/bpf: add stress test for large programs”
- Body 说明测试的极端场景
- 签名完整
Patch 17/18: bpf: optimize memory allocation in patcher
文件修改:
-
kernel/bpf/verifier.c- 优化内存分配
编译验证:
- 两种配置都编译通过
- 无编译警告
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 内存池实现正确
- 无内存泄漏
功能验证:
- 性能提升(减少 kmalloc 调用)
- 功能正确(所有测试通过)
性能测试:
- pyperf180.bpf.o 性能测试
- 验证时间减少 30-40%
- 内存使用合理
Commit Message:
- Subject: “bpf: optimize memory allocation in patcher”
- Body 说明优化方法和性能数据
- 签名完整
Patch 18/18: bpf: remove array-based patching implementation
文件修改:
-
kernel/bpf/Kconfig- 删除CONFIG_BPF_VERIFIER_LINKED_LIST -
kernel/bpf/verifier.c- 删除所有#ifdef和#else分支 -
kernel/bpf/verifier.c- 删除旧的数组实现函数 -
kernel/bpf/core.c- 删除bpf_patch_insn_single()
编译验证:
-
make kernel/bpf/verifier.o- 编译通过 -
make kernel/bpf/core.o- 编译通过 - 无编译警告
- 无未使用的函数
代码质量:
-
./scripts/checkpatch.pl- 无警告 - 代码简洁(删除约 300 行)
- 无死代码
功能验证:
- 所有 selftests 通过
- 所有内核 BPF 测试通过
- 无功能回归
性能测试:
- pyperf180.bpf.o 最终性能测试
- 验证时间减少 30-40%
- 与 baseline 对比
文档更新:
- 更新相关文档
- 更新 commit message 引用
Commit Message:
- Subject: “bpf: remove array-based patching and enable linked list by default”
- Body 说明删除的代码量
- Body 包含最终性能数据
- Body 说明这是系列的最后一个补丁
- 签名完整
整体验证清单
Bisectability 验证
- 运行 bisectability 验证脚本
- 每个补丁都能独立编译(两种配置)
- 每个补丁都能独立运行
-
git bisect可用
性能验证
- Baseline 测试(Patch 1 之前)
- 中期测试(Patch 8 之后)
- 最终测试(Patch 18 之后)
- 性能提升达到 30-40%
功能验证
- 所有 BPF selftests 通过
- 内核 BPF 测试套件通过
- 无功能回归
- 无内存泄漏
代码质量
- 所有补丁通过
checkpatch.pl - 代码风格一致
- 注释完整
- 无死代码
文档
- 设计文档完整
- Commit message 清晰
- 性能数据完整
- 参考资料完整
社区准备
- Cover letter 准备完成
- 性能数据图表准备
- 准备好回答审查意见
- 准备好迭代多个版本
验证脚本
Bisectability 验证脚本
1 | |
性能测试脚本
1 | |
提交策略
RFC(第 1-8 周)
1 | |
V1(第 9-16 周)
1 | |
V2-VN(第 17-24 周)
1 | |
代码变更统计
- 新增代码:约 1,500 行(链表实现 + 测试)
- 删除代码:约 300 行(旧数组实现)
- 净增加:约 1,200 行
总结
修正后的方案保证:
- ✅ 每个补丁都能编译通过(两种配置)
- ✅ 每个补丁都能独立运行
- ✅ git bisect 可用
- ✅ 默认行为不变(直到 Patch 18)
- ✅ 可以 A/B 对比(Patch 7-17)
关键技术:
__maybe_unused标记(Patch 1-6)#ifdef CONFIG_BPF_VERIFIER_LINKED_LIST(Patch 7-17)- 渐进式启用(Patch 18 才默认启用)
这是 Linux 内核开发的标准做法,完全符合社区要求!
附录:数据结构选择决策树
1 | |
附录:性能对比矩阵
理论性能(N=10,000, M=5,000)
| 方案 | 修补阶段 | 线性化阶段 | 总操作数 | 相对提升 |
|---|---|---|---|---|
| 当前数组 | 50,000,000 | 0 | 50,000,000 | 基准 |
| 批量修补 | 10,000,000 | 0 | 10,000,000 | 5× |
| 链表 | 5,000 | 10,000 | 15,000 | 3,333× |
| Gap Buffer | 25,000,000 | 0 | 25,000,000 | 2× |
| Rope | 65,000 | 10,000 | 75,000 | 667× |
| 分块数组 | 640,000 | 0 | 640,000 | 78× |
| COW | 5,000 | 15,000 | 20,000 | 2,500× |
实际性能(考虑常数因子和缓存)
| 方案 | 预期提升 | 实现难度 | 内存开销 | 缓存友好 | 综合评分 |
|---|---|---|---|---|---|
| 当前数组 | 0% | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ❌ |
| 批量修补 | 10-15% | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⚠️ |
| 链表 | 30-40% | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ✅ |
| Gap Buffer | 15-20% | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⚠️ |
| Rope | 35-45% | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | ❌ |
| 分块数组 | 20-25% | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⚠️ |
| COW | 25-35% | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⚠️ |
说明:
- ⭐ 越多越好(实现难度除外,越少越好)
- ✅ 推荐,⚠️ 可考虑,❌ 不推荐
附录:如果选择其他方案
如果选择 Gap Buffer
适用场景:
- 不想引入链表复杂度
- 修补位置有一定局部性
- 作为过渡方案
实施计划(2 个补丁):
- Patch 1: 实现 Gap Buffer 数据结构
- Patch 2: 替换 bpf_patch_insn_data 实现
预期收益:15-20% 提升
风险:治标不治本,最终可能还要做链表
如果选择分块数组
适用场景:
- 想要折中方案
- 不想完全放弃数组的缓存友好性
- 可以接受中等提升
实施计划(3 个补丁):
- Patch 1: 实现分块数据结构
- Patch 2: 实现分块修补逻辑
- Patch 3: 替换 bpf_patch_insn_data 实现
预期收益:20-25% 提升
风险:实现复杂度接近链表,但收益不如链表
为什么不推荐 Rope
理由:
- 实现复杂度太高(需要平衡树算法)
- 内存开销大(树节点元数据)
- 缓存不友好(指针跳转)
- 收益有限(O(M log N) vs O(M+N),差异不大)
- 过度设计(BPF verifier 不需要这么复杂)
计算:
- 链表实现:约 1,500 行代码
- Rope 实现:约 3,000 行代码(包括平衡树)
- 性能差异:链表 15,000 操作 vs Rope 75,000 操作
- 链表仍然快 5 倍,且代码少一半
为什么不推荐 COW
理由:
- BPF verifier 不需要回滚功能
- 合并阶段处理重叠 delta 复杂
- 内存开销大(保存所有 delta)
- 收益与链表相当,但实现更复杂
如果需要回滚:
- 可以考虑 COW
- 但 BPF verifier 的验证是单向的,不需要回滚
文档版本:v1.1
日期:2026-01-22
作者:Qiliang Yuan realwujing@gmail.com
测试环境:Linux 6.19.0-rc5
更新日志:
- v1.1 (2026-01-22): 添加其他数据结构方案对比分析(Gap Buffer、Rope、分块数组、COW)
- v1.0 (2026-01-22): 初始版本,链表方案设计和 18 个补丁规划