本文地址:https://www.ebpf.top/post/lwn-tracing-multi

原文地址:https://lwn.net/Articles/1082948/
作者:Daroc Alden
发布时间:2026-07-22
翻译/整理:狄卫华

想给每一个系统调用都挂上延时统计时,过去常见两条路:要么写几百份几乎一样的 BPF 程序,要么退回 kprobe 多重挂接——安装灵活,但热路径往往更贵。Jiri Olsa 推动的 tracing_multi 改变了这件事:一条程序、一个 pattern,就能挂到一批跟踪点上。这项工作在 2026 年 LSFMM+BPF 上讨论,随后合入主线,随 Linux 7.2(2026-08-16)一起落地。

本文按 Daroc Alden 在 LWN 的会议报道梳理设计取舍,并补上我们在 7.2 上的复现结论:官方 tracing_multi_test 13/13 通过;用 BTF id + 黑名单一次挂约 448 个 __x64_sys_*,read 平均延时约 3.6us。

1. 为什么还要动跟踪点

内核跟踪点(tracepoint)适合调试、主动监控和性能测量。但在 tracing_multi 之前,任意给定的 BPF 程序只能挂到单个跟踪点。

同类能力里,kprobe 早就支持多重挂接,功能上也能覆盖跟踪点的大部分场景。Olsa 强调差别在性能:跟踪点安装更慢,执行却明显更快。他在会上展示的幻灯片给出了每秒百万次量级的对照——按具体挂法不同,跟踪点可以大约快一倍。

如果跟踪点能在支持多重挂接的同时不回退这条热路径优势,它就会成为监控性能敏感操作的更有吸引力的选项。这正是 tracing_multi 要解决的问题。

2. 从「一对一蹦床」到「一组一起管」

跟踪点靠运行时代码修补,把执行从被跟踪函数拐到生成的蹦床(trampoline):蹦床先跑已挂接的钩子,再回到原函数。

早期实现走旧的 ftrace API,蹦床、函数和 ftrace 对象 一一对应——天然不适合「一个程序挂很多点」。董梦龙曾试过全局共用一个蹦床;蹦床每次都要分辨「刚才是谁调进来的」,开销太高,性能回退明显。

新做法改用更新的 ftrace API:单个 ftrace 对象可以配置多个函数。每个函数仍有自己的蹦床,但管理可以按组进行。蹦床生成很快,一次重生成若干个并不会带来很大的安装开销。

3. 开发里两道硬坎:锁与热补丁

多重挂接把「同时改很多蹦床」变成常态。最初每个蹦床一把锁,批量修改时容易撞上 lockdep 的 48 把锁上限。Andrii Nakryiko 的解法是去掉每蹦床私有锁,换成按蹦床地址共享的 32 把锁池;锁只短暂持有,实践中并发几乎不差。

另一道坑由 Sashiko 补丁评审系统揪出:跟踪点与 live kernel patch 都走 ftrace。热补丁替换被跟踪函数时,ftrace 会通知跟踪点重生成蹦床——这时要拿蹦床锁,但线程可能已持有 ftrace 侧其他锁,顺序反了就是死锁。缓解办法是循环尝试取锁,迭代间睡眠以暂时放下已持有的 mutex。

真正棘手的是热补丁卸除:蹦床要改回原址,却不能再走「循环取锁」那条路径。若卸除瞬间蹦床锁正被持有,蹦床可能没改回去。Steven Rostedt 建议返回 EAGAIN,让卸除路径放锁重试;也有人认为这只是多一次间接跳转的性能问题,不是正确性事故。会议报道把分歧原样记下,提醒读者:合入不等于所有边界场景都有「完美故事」。

挂接接口相对直接——程序指针、匹配跟踪点的 pattern,以及可选的一组 cookie,让程序知道自己是从哪个跟踪点进来的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct bpf_link *
bpf_program__attach_tracing_multi(
    const struct bpf_program *prog,
    const char *pattern,
    const struct bpf_tracing_multi_opts *opts);

struct bpf_tracing_multi_opts {
    size_t sz;
    __u32 *ids;
    __u64 *cookies;
    size_t cnt;
    size_t :0;
};

支持函数入口、出口,或两者都跟踪。当前不支持改写返回值,也不支持挂到 LSM 钩子。Nakryiko 问这是设计天花板还是分期交付;Olsa 确认更复杂类型以后可以做,只是想先把最简单的情形做稳——撰稿时他还没有贴出后续补丁。

5. 验证器的硬边界:能读参数,不能解引用指针

单点挂接时,验证器知道目标函数签名,允许安全地解引用传入的内核对象指针。多点挂接时,目标函数签名可能各不相同,验证器无法在一遍检查里核对这些访问。

因此:挂到多个跟踪点的程序可以加载被跟踪函数的参数,但不能解引用在那里找到的任何指针。Olsa 承认这很难放开——若为放开而拖慢新 API,就抵消了大半收益。

我们在样例 syscall_lat 里遵守同一约束:只记 bpf_get_func_ip() 与耗时,不去读 syscall 的 fd/buf。glob 挂载时 attach cookie 往往全是 0,不能当 syscall 编号,必须以 func_ip 做 map key,用户态再用 kallsyms 译名。

6. 规模预期与 Arm 上的 IPI

会上有人问:一次会挂多少函数?创建蹦床时会不会狂发 IPI?Olsa 估计多数用户最多挂到几百个跟踪点——例如钩住每一个系统调用实现。ftrace 在 x86 上用了特有技巧,避免「挂一个点发一次 IPI」;Arm 上暂不成立,但补支持相对直接。

Rostedt 说 x86 那段是他写的;给 Arm 加支持主要是另一种蹦床。被追问时他解释:Arm 没有像 x86 call 那样「压栈 + 跳转」的单条指令,bl 把返回地址留在寄存器,补丁往往要改多条指令,就必须 IPI 保证 CPU 看不到中间态。若有人实现「单次原子写打进蹦床、又遵守 Arm 可执行代码修补规则」的方案,就能复用 x86 的避 IPI 技巧。Rostedt 原话大意是:有人做的话,他不会 NAK。撰稿时还没有人贡献这样的 Arm 蹦床;Olsa 的补丁集已于 2026-06-07 合入。

7. 我们在 Linux 7.2 上的复现

仓库 samples/tracing_multi_net/ 记录了在 Multipass k72(7.2.0-070200-generic)上的验证:

项 结果
test_progs -t tracing_multi_test 13/13 OK(含 module、session)
tracing_multi_bench_attach 全量 kallsyms 可能 FAIL,不否定功能
syscall_lat 挂 __x64_sys_* BTF id + 黑名单后约 448 个一次挂上
read 延时 avg ~ 3.6us(含睡眠路径的 syscall 会把直方图拉宽,属预期)

实操里几条容易踩坑的经验:

  1. uname 是 7.2 只说明版本号;BTF dump、模块 BTF、headers 是否带 vmlinux 都可能和 selftests 假设不一致。
  2. 全量 kallsyms 或未过滤的 __x64_sys_* glob 容易 -EINVAL;优先 BTF id + 黑名单。
  3. 测 syscall 延时更适合 fsession.multi(内核为「这一次调用」留 8 字节 session cookie 槽),不要和 fentry.multi/fexit.multi 两套程序同时挂。

核心探针侧大致是:入口写时间戳到 bpf_session_cookie(),返回路径读出做差,再按 bpf_get_func_ip() 归桶——完整代码见同目录 syscall_lat.bpf.c。

8. 总结

tracing_multi 把「跟踪点只能单挂」这条历史限制拆掉了:新 ftrace 分组管理蹦床,锁池与热补丁路径上的坑被明确写进合入过程,用户态拿到的是 pattern + cookie 这一套相对干净的 API。代价也很清楚——验证器不允许解引用多签名下的参数指针;Arm 上的避 IPI 优化仍等人补蹦床。

对我们这类「想一次看清整类内核热点」的观测场景,它终于让跟踪点在多挂接维度上追上了 kprobe 的便利,同时尽量保住执行路径上的速度优势。若你手头已有 7.2,直接跑官方 tracing_multi_test,再试 syscall_lat,比只读会议纪要更有体感。

本文据 LWN 会议报道整理并补充 7.2 实测;完整原文见文中已给出的链接。

延伸阅读

若还想顺着读,可看这几篇:

  1. 使用eBPF编写系统调用跟踪器
  2. BPF 高性能稳定跟踪机制 rawtracepoint 介绍和样例
  3. eBPF 慢在哪? perf 把热路径开销测清楚