第6篇:BPF 程序运行 — BPF_PROG_TYPE_KPROBE/TRACEPOINT/PERF_EVENT 与 bpf_trace.c¶
源码:
kernel/trace/bpf_trace.cnet/core/filter.c| 头文件:include/uapi/linux/bpf.h
系列目录:eBPF 内核源码深度解析
1. BPF 程序的三类内核 entry point¶
| prog_type | 触发点 | 上下文 | 典型 helper |
|---|---|---|---|
BPF_PROG_TYPE_KPROBE |
kprobe 断点 | 任意内核上下文 | bpf_get_current_pid_tgid, bpf_get_current_comm |
BPF_PROG_TYPE_TRACEPOINT |
tracepoint 埋点 | 触发 tracepoint 的上下文 | bpf_get_smp_processor_id, bpf_get_current_cgroup_id |
BPF_PROG_TYPE_PERF_EVENT |
perf_event 溢出 | NMI 上下文 | 只允许 map 操作(不能再调度) |
1.1 BPF_PROG_TYPE_KPROBE 的注册¶
// kernel/trace/bpf_trace.c
// Kprobe BPF 程序的 verifier 批准的 helper 集合:
bpf_kprobe_multi_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
switch (func_id) {
case BPF_FUNC_get_current_pid_tgid: // 获取当前 PID/TID
case BPF_FUNC_get_current_uid_gid: // 获取 UID/GID
case BPF_FUNC_get_current_comm: // 获取当前进程名
case BPF_FUNC_perf_event_output: // 向 perf_event 发送输出
case BPF_FUNC_get_stackid: // 获取栈 ID
case BPF_FUNC_get_stack: // 获取用户/内核栈
case BPF_FUNC_ktime_get_ns: // 获取当前时间
case BPF_FUNC_trace_printk: // 向 ftrace ring buffer 写日志
case BPF_FUNC_map_lookup_elem: // map 查找
case BPF_FUNC_map_update_elem: // map 更新
// ... 40+ helpers ...
}
}
1.2 kprobe_multi —— 一个 BPF 程序同时探测多个函数¶
// kernel/trace/bpf_trace.c:1281-1303
static bool is_kprobe_multi(const struct bpf_prog *prog)
{
return prog->type == BPF_PROG_TYPE_KPROBE &&
(prog->expected_attach_type == BPF_TRACE_KPROBE_MULTI ||
prog->expected_attach_type == BPF_TRACE_KPROBE_SESSION);
}
// BPF_TRACE_KPROBE_MULTI:
// 一个 BPF 程序附加到多个内核函数,例如:
// bpftrace -e 'kprobe:tcp_* { @bytes = count(); }'
// 自动探测所有 tcp_ 前缀的函数
1.3 kprobe_session —— 入口+出口对¶
// kernel/trace/bpf_trace.c:1287-1291
static bool is_kprobe_session(const struct bpf_prog *prog)
{
return prog->type == BPF_PROG_TYPE_KPROBE &&
prog->expected_attach_type == BPF_TRACE_KPROBE_SESSION;
}
// BPF_TRACE_KPROBE_SESSION:
// 一个 BPF 程序在函数入口执行,另一个在出口执行。
// 两个程序共享同一个 map,实现"entry 记录开始 → exit 计算耗时"。
2. BPF_PROG_TYPE_TRACEPOINT¶
// kernel/trace/bpf_trace.c
bpf_tracing_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
switch (func_id) {
case BPF_FUNC_map_lookup_elem:
case BPF_FUNC_map_update_elem:
case BPF_FUNC_map_delete_elem:
case BPF_FUNC_map_push_elem:
case BPF_FUNC_map_pop_elem:
case BPF_FUNC_map_peek_elem:
case BPF_FUNC_get_current_pid_tgid:
case BPF_FUNC_get_current_uid_gid:
// ...
}
}
tracepoint BPF 程序能读取 tracepoint 的 ctx 参数。例如 tracepoint:net:netif_receive_skb — BPF 程序可以直接访问 skb 指针。
3. BPF_PROG_TYPE_PERF_EVENT¶
// Perf_event BPF 程序只有*非常受限*的 helper 集合:
// 原因是 perf event 在 NMI 上下文中运行,不能睡眠、不能调度、不能拿 mutex
bpf_perf_event_func_proto(enum bpf_func_id func_id, ...)
{
// 允许: map_lookup_elem, map_update_elem, perf_event_output
// 禁止: get_current_pid_tgid, kmalloc, spin_lock, 等
}
典型用途:Netflix 的 flame graph 工具——BPF 程序在 perf stat -e cycles 的每个采样中运行,聚合到 map 中,用户态定期 dump map。
4. BPF_PROG_TYPE_XDP 的 helper¶
// net/core/filter.c:3992
BPF_CALL_2(bpf_xdp_adjust_head, struct xdp_buff *, xdp, int, offset)
{
// 移动 xdp->data 指针:允许 BPF 程序在包前添加/移除头部
// 支持 IPIP/GRE 解封装、VLAN 标签操作等
}
// net/core/filter.c:4668
BPF_CALL_3(bpf_xdp_redirect_map, struct bpf_map *, map, u64, key, u64, flags)
{
// 将 XDP 处理后的包重定向到其他网卡或 CPU
// map 类型: BPF_MAP_TYPE_XSKMAP, BPF_MAP_TYPE_DEVMAP, BPF_MAP_TYPE_CPUMAP
}
// XDP 程序可用的其他 helper:
// bpf_xdp_adjust_tail — 在包尾添加/移除数据
// bpf_xdp_redirect — 重定向到单个网卡
// bpf_xdp_get_buff_len — 获取包长度
// bpf_xdp_load_bytes / bpf_xdp_store_bytes — 安全地读写包数据
5. BPF 程序的完整生命周期¶
用户态: bpftrace -e 'kprobe:do_sys_open { printf("%s\n", str(args->filename)); }'
→ bpftrace 解析脚本
→ 生成 BPF bytecode (LLVM/BPF CO-RE)
→ bpf(BPF_PROG_LOAD, prog_type=BPF_PROG_TYPE_KPROBE, ...)
→ bpf_prog_load
→ bpf_check (verifier)
→ 检查所有 helper 是否允许在这个 BPF_PROG_TYPE 中使用
→ 追踪所有寄存器和栈
→ bpf_prog_select_runtime (JIT)
→ 返回 fd
→ perf_event_open(PERF_TYPE_KPROBE, kprobe_func="do_sys_open")
→ register_kprobe → int3 写入
内核中 do_sys_open 调用:
→ int3 → kprobe_int3_handler
→ aggr_pre_handler (遍历所有附加的 BPF 程序)
→ bpf_prog_run(prog, ctx)
→ BPF 已 JIT 编译(直接跳转) → x86 原生代码 → 写入 map
→ 或 解释器执行(无 JIT)
→ 单步执行原指令
6. BPF 与 perf/ftrace/tracepoint 的关系¶
┌──────────────────────────────────────────────┐
│ 探测点 │
│ int3 (kprobe/uprobe) / tracepoint 调用 │
└──────────────────┬───────────────────────────┘
│
┌──────────────────┼───────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐
│ ftrace │ │ perf │ │ BPF │
│ │ │ │ │ │
│ trace_kprobe │ │ perf_kprobe_ │ │ BPF_PROG_TYPE_KPROBE │
│ trace_uprobe │ │ init │ │ │
│ │ │ perf_uprobe_ │ │ bpf_trace_kfuncs_init │
│ 通过 trace_ │ │ init │ │ (注册允许的 helper) │
│ probe_register│ │ │ │ │
│ 回调 │ │ 通过 perf_trace│ │ 通过 bpf_prog_run │
│ │ │ _event_init │ │ 回调 │
└───────────────┘ └───────────────┘ └───────────────────────┘
│ │ │
└──────────────────┼────────────────────┘
│
▼
内核基础: register_kprobe() + kprobe_int3_handler()
三者共享相同的底层探测机制(kprobe/uprobe/tracepoint),但在上层提供不同的用户接口和数据处理路径。
系列结语¶
6 篇文章从 struct perf_event 和系统调用起步,到 ring buffer 的零拷贝 mmap 轮询,再到 x86 Intel PMU 驱动 NMI 采样,再深入到 tracepoint/ftrace 桥接,最后覆盖 kprobe/uprobe 两大动态探测机制,完整覆盖了内核 perf_event 子系统和 BPF 跟踪程序的实现。
| 篇 | 内容 | 核心文件 |
|---|---|---|
| 01-perf-event | struct perf_event + syscall | include/linux/perf_event.h:765, core.c:13844 |
| 02-ring-buffer | mmap + 零拷贝 | core.c:6825, ring_buffer.c |
| 03-pmu | x86 PMU + NMI | arch/x86/events/intel/core.c |
| 04-tooling | tracepoint/ftrace 集成 | trace_event_perf.c |
| 05-kprobe-uprobe | kprobe/uprobe 探测 + perf bridge | kprobes.c:1708, uprobes.c:1390, trace_event_perf.c:247 |
| 06-bpf-progs (本篇) | BPF 程序运行 | bpf_trace.c, filter.c |