在 Linux 内核的技术演进中,跟踪点(Tracepoint) 与 kprobe 一向是系统可观测性与性能诊断的双璧。相比于基于断点/异常机制的 kprobe,跟踪点采用运行时代码补丁(Code Patching)和静态插桩机制,拥有极高的执行效率。
然而,长期以来跟踪点存在一个令人困扰的痛点:一个 BPF 程序无法高效地批量附加(Attach)到多个跟踪点上 。若要监控数百个系统调用或内核事件,开发者只能在用户态循环调用 bpf_link_create,导致极其高昂的初始化延迟与内核资源消耗。
在 LWN 报道的 2026 年 Linux 存储、文件系统、内存管理与 BPF 峰会(LSMMS/BPF Summit)上,Jiri Olsa 展示并宣布了他的重磅突破------通过引入 基于新 ftrace API 的分组管理方案 ,实现了单个 BPF 程序对多个跟踪点的高效批量附加。该特性已正式合并至 mainline 内核,并包含在 Linux 7.2 版本中。
本文将深度拆解新的 ftrace 分组管理架构、Jiri Olsa 的应用设计、其优缺点以及未来的发展趋势。
一、 核心痛点与历史演进
要理解 Olsa 的创新,需要先弄清内核将 BPF 挂载到内核函数/跟踪点时的历史演进逻辑。内核在底层利用 ftrace 的 Direct Call 机制,将目标函数的入口指令替换为跳转到 BPF 生成的跳板程序(Trampoline)。
在 Olsa 方案之前,社区尝试过两种技术路线:
路线 1: 1-to-1 方案 (经典 ftrace direct)
[被跟踪函数 A] ---> [Trampoline A] <-- (配置: ftrace_ops A)
[被跟踪函数 B] ---> [Trampoline B] <-- (配置: ftrace_ops B)
问题: 1000个函数需要1000个 ftrace_ops,注册与代码补丁效率极低。
路线 2: 全局跳板方案 (Dong Menglong 尝试)
[被跟踪函数 A] --\
[被跟踪函数 B] ---> [全局 Trampoline] (运行时查表判别 Caller) <-- (配置: 1个 ftrace_ops)
[被跟踪函数 C] --/
问题: 解除了注册瓶颈,但运行时查表识别调用者开销巨大,导致执行性能严重衰退。
-
方案 1:旧的 1 对 1 模式
每个挂载点拥有独立的
ftrace_ops(ftrace 配置实体)与 Trampoline。若附加到 1000 个跟踪点,用户态与内核需交互 1000 次,且内核必须挨个处理 1000 个ftrace_ops的代码补丁,初始化极慢。 -
方案 2:全局单一跳板模式(Menglong Dong 的尝试)
使用单个全局
ftrace_ops和单个全局 Trampoline 接收所有流量。虽然解决了注册慢的问题,但跳板程序在运行时必须在 CPU 寄存器/栈帧中判别"到底是哪个函数触发了调用",引入了显著的运行时性能衰退,丧失了 Tracepoint 相较于 kprobe 的性能优势。
二、 核心机制:新的 ftrace API 分组管理方案
Olsa 采纳并完善的新 ftrace API 彻底打破了"注册慢"与"运行慢"的两难困境,其核心哲学是:"配置统一分组,执行按需独立"。
┌─────────────────────────────────────────┐
│ 全局分组 ftrace_ops │
│ (用 Multi-Direct Hash 表记录映射关系) │
└────────────────────┬────────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 被跟踪函数 A │ │ 被跟踪函数 B │ │ 被跟踪函数 C │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ (Direct Call) │ (Direct Call) │ (Direct Call)
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 专属跳板 A │ │ 专属跳板 B │ │ 专属跳板 C │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└─────────────────────────┼─────────────────────────┘
▼
┌──────────────────────┐
│ 目标 BPF 程序 (单例) │
└──────────────────────┘
1. 配置层:Single Ops / Multi-Direct Hash
不再为每个挂载点生成一个 ftrace_ops,而是通过 单个分组 ftrace_ops 管理所有批量附加的跟踪点。
在该 ftrace_ops 内部维护一个 Multi-Direct 哈希表,记录各个被跟踪函数(IP 地址)与其专属 Trampoline 之间的映射。在进行代码补丁更新时,内核仅需执行一次批量代码更新 API,即可同时重定向数百个函数的指令。
2. 执行层:Per-Function Independent Trampoline
尽管配置是批量化的,但内核依然为每一个被跟踪函数单独生成定制的 Trampoline。
这意味着当函数被调用时,直接跳转到其专属 Trampoline,跳板中硬编码了该函数的参数传递逻辑和目标 BPF 程序的入口。没有任何运行时查表判断,完全保留了传统 Tracepoint 零额外损耗的执行性能。
3. 配套重构:锁池化(Lock Pooling)与并发优化
批量生成与修改海量 Trampoline 时,传统"一跳板一锁"的做法会导致并发修改时持有的锁数量暴增,从而触发内核调试工具 lockdep 的 48 把锁上限。
针对这一问题,Andrii Nakryiko 重构了锁机制:取消 Trampoline 内部的单独锁,引入一个包含 32 把共享锁的哈希锁池(Lock Pool) ,根据 Trampoline 的内存地址将锁分散映射。这不仅消除了 lockdep 超限警告,还因锁持有时间极短而保留了极高的并发更新能力。
三、 Jiri Olsa 如何实现 BPF 的多跟踪点附加?
借助上述 ftrace API 分组方案,Jiri Olsa 在 BPF 子系统中构建了全新的批量附加接口。
1. 用户态 API 与结构体
用户态新增了 bpf_program__attach_tracing_multi 接口,支持通过正则表达式匹配模式(pattern)或指定 ID 列表批量绑定:
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; // 目标跟踪点的 ID 数组
__u64 *cookies; // 传递给 BPF 程序的自定义上下文 cookie 数组
size_t cnt; // 数量
size_t :0;
};
2. 上下文识别:Cookies 机制
由于单个 BPF 程序被多个跟踪点共享,BPF 程序在执行时往往需要知道自己"当前是被哪一个跟踪点调用的"。
Olsa 的实现允许在附加时传入一个 cookies 数组。在生成各个函数的专属 Trampoline 时,内核会将对应的 Cookie 值作为常数硬编码/注入到该跳板的上下文传递指令中。BPF 程序只需调用 bpf_get_attach_cookie() 辅助函数,即可在零运行时开销的前提下精确获知当前的触发源。
四、 方案优缺点深度剖析
优点
-
极致的初始化与执行性能 :批量注册时间从原本的秒级/百毫秒级降至毫秒级;运行期速度依旧比
kprobe_multi快近一倍。 -
内核资源占用低 :避免了大量
bpf_link和ftrace_ops带来的内存碎片与管理开销。 -
接口简单优雅 :支持通过
pattern(如sys_enter_*)一键挂载成百上千个系统调用,大幅简化了可观测性工具(如 Tetragon、BCC、Cilium)的开发复杂度。
缺点与局限性
-
Verifier(验证器)解引用限制:
-
单点附加 :Verifier 能精确知道单一函数的签名,因此允许 BPF 程序直接安全地解引用(dereference)参数中的内核结构体指针(如
struct task_struct *)。 -
多点附加 :由于被挂载的多个函数签名可能各不相同,Verifier 无法在单次 pass 中完成所有签名的类型校验。因此,多点附加的 BPF 程序仅被允许读取原始参数值,不允许直接解引用其中的内核对象指针。
-
-
能力覆盖尚有空白:
-
目前仅支持跟踪函数的入口(entry)和出口(exit)。
-
暂不支持修改返回值(
fmod_ret)以及挂载到 Linux 安全模块(LSM)钩子上。
-
-
架构间性能存在差异(x86 vs Arm):
-
x86 架构 :
call指令为 5 字节原子写,结合 ftrace 的黑科技,在批量修改代码时无需发送核间中断(IPI),耗时极短。 -
Arm64 架构 :Arm 的
bl(branch-and-link)等指令替换需要修改多条指令,为保证代码一致性必须发送 IPI 刷新流水线,导致在 Arm 平台批量附加时的开销高于 x86。
-
五、 现状与未来展望
1. 现状
-
内核合并状态 :该核心补丁集已被合并至 Linux mainline,并在 Linux 7.2 内核中正式亮相。
-
死锁防护落地 :针对 Sashiko 补丁审查系统发现的"跟踪点与内核热补丁(Livepatch)交互时的锁颠倒死锁"问题,社区已通过在循环中尝试持锁并在失败时返回
EAGAIN让出互斥锁的方式解决了死锁隐患。
2. 未来发展方向
-
Arm64 单指令原子跳板优化:正如 Steven Rostedt 和 Jiri Olsa 在 LSMMS 峰会上讨论的,一旦有人为 Arm64 实现符合代码补丁规范的单指令原子写入跳板,Arm 架构也将享受到与 x86 相同的"无 IPI"高速批量附加体验。
-
Verifier 类型的增强:社区正在探讨如何在不牺牲 Verifier 检查性能的前提下,引入更聪明的多类型签名推断机制,以期未来能部分放开对指针解引用的限制。
-
覆盖更多挂载类型 :随着基础架构的稳定,后续将逐步拓展对 LSM 钩子和返回值修改(
fmod_ret)的多点支持。
结语
Jiri Olsa 主导的"BPF 多跟踪点附加"不仅是一次 BPF 子系统的 API 升级,更是一次对内核 ftrace 基础架构、代码补丁并发控制以及锁机制的深度重构。
随着 Linux 7.2 内核的发布,云原生安全与可观测性工具将彻底告别在跟踪点性能与批量附加灵活性之间的艰难抉择,Linux 内核的动态追踪能力也由此迈上了一个新的台阶。
