eBPF 学习路线:从 libbpf-bootstrap minimal 到 sched_ext
最近开始系统学习 eBPF 后,我发现它最容易踩的坑不是“代码写不出来”,而是概念一下子铺得太开。
一开始会同时遇到 tracepoint、kprobe、BPF Map、Ring Buffer、BTF、CO-RE、XDP、sched_ext……每个词都能继续展开一大堆内容。如果没有一个明确顺序,很容易变成“每个都看过一点,但真正写程序时还是不知道从哪里下手”。
所以我把自己的学习路线重新整理了一遍。目标不是刷完多少文档,而是按下面这个节奏推进:
能运行
↓
能看懂
↓
能修改
↓
能自己写
最终希望做到的不只是“会跑 eBPF 示例”,而是能够独立完成一个小型 eBPF 项目,再继续往性能分析、网络和 sched_ext 这些方向深入。
1. 先看完整路线
我目前给自己安排的顺序是:
Linux / C 基础
↓
libbpf-bootstrap minimal
↓
理解 eBPF 运行机制
↓
tracepoint
↓
kprobe / kretprobe
↓
uprobe / uretprobe
↓
BPF Map
↓
Ring Buffer
↓
BTF / CO-RE
↓
完成一个独立项目
↓
性能分析 / XDP / sched_ext
这个顺序的核心思路是:先把最小程序跑起来,再沿着这个程序把编译、加载、挂载、数据交换一步步拆开。
2. 第一阶段:补够用的 Linux 和 C 基础
学 eBPF 不需要先把整个 Linux 内核啃完,但有些基础绕不过去。
C 语言至少要比较熟悉:
- 指针和二级指针;
- 结构体、结构体指针;
- 数组;
- 宏;
- 位运算;
- 类型转换;
sizeof;static、const、volatile;- 基本的内存布局。
后面经常会看到类似:
struct task_struct *task;
以及:
task->pid
如果结构体、指针还不熟,继续往下看 BTF、CO-RE 只会更痛苦。
Linux 这边,我认为最重要的是先把这条路径理解清楚:
用户态程序
↓
syscall
↓
Linux 内核
↓
内核函数 / tracepoint
例如:
write(fd, buf, len);
用户态调用 write() 后会进入内核。eBPF 很多 tracing 程序,本质上就是在这条执行路径上的某个位置“挂一个观察点”。
除了系统调用,进程、线程、PID、文件描述符、虚拟内存、上下文切换、调度器这些概念也值得提前熟悉。
3. 第二阶段:不要急着写大项目,先把 minimal 跑通
我比较推荐从官方生态里的 libbpf-bootstrap 开始:
https://github.com/libbpf/libbpf-bootstrap
入门先看:
examples/c/minimal
这个例子很小,但里面已经包含了 eBPF 开发最重要的结构。
核心文件只有两个:
minimal.c
minimal.bpf.c
它们分别运行在两个世界:
minimal.c
↓
用户空间
minimal.bpf.c
↓
Linux 内核
这件事必须先建立直觉。很多后面的 API 看起来复杂,其实都围绕“用户态负责管理,eBPF 程序在内核事件上执行”这个模型展开。
4. 先读懂 minimal.bpf.c:eBPF 程序到底什么时候运行
一个典型片段是:
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\n", pid);
return 0;
}
这里最值得先搞清的是三件事。
4.1 SEC(...) 决定挂载位置
SEC("tp/syscalls/sys_enter_write")
表示这个 eBPF 程序对应 sys_enter_write tracepoint。
因此执行链可以先简单理解成:
程序调用 write()
↓
进入 Linux 内核
↓
sys_enter_write
↓
handle_tp()
4.2 bpf_get_current_pid_tgid() 获取当前进程/线程信息
常见写法:
int pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_pid_tgid() 返回一个 64 位值,高 32 位是 TGID,通常也就是我们平时说的进程 PID;低 32 位对应线程 ID。
4.3 bpf_printk() 只把它当调试工具
例如:
bpf_printk("hello\n");
调试阶段可以从:
sudo cat /sys/kernel/debug/tracing/trace_pipe
查看输出。
部分系统路径是:
sudo cat /sys/kernel/tracing/trace_pipe
真正做高频监控时,不应该把所有事件都靠 bpf_printk() 打出来。
5. 再读 minimal.c:用户态到底在做什么
minimal.c 这边主要负责 eBPF 对象的打开、加载和挂载。
一个典型流程是:
open
↓
load
↓
attach
↓
运行 / 与 eBPF 通信
常见代码:
skel = minimal_bpf__open();
minimal_bpf__load(skel);
minimal_bpf__attach(skel);
可以分别理解成:
open
minimal_bpf__open();
在用户态创建并打开对应的 BPF skeleton,为后续加载做准备。
load
minimal_bpf__load(skel);
把 eBPF 程序真正送入内核,并经过 Verifier 检查。
attach
minimal_bpf__attach(skel);
把已经加载的程序挂到 SEC(...) 声明的 hook 上。
所以用户态和内核态的整体关系就是:
用户态程序
↓
libbpf
↓
Linux 内核
↓
eBPF 程序
这一步看懂后,再学 Map、Ring Buffer、CO-RE 会顺很多。
6. 第三阶段:搞清 eBPF 程序是怎么被执行的
接下来要把“源代码为什么能在内核里跑”理顺。
我目前用这条链路来记:
eBPF C
↓
Clang
↓
BPF Bytecode
↓
Verifier
↓
JIT
↓
CPU 本地机器码
6.1 为什么 eBPF 经常用 Clang
普通 C 程序 GCC、Clang 都能编译。
但 eBPF 程序通常使用 Clang:
clang -target bpf -c minimal.bpf.c -o minimal.bpf.o
关键是:
-target bpf
它告诉编译器:目标不是 x86 或 ARM,而是 BPF 指令集。
6.2 Verifier 是一道必须过的门
eBPF 程序不能像普通内核代码一样直接执行。进入内核前,Verifier 会检查它是否满足安全要求。
常见检查包括:
- 非法指针访问;
- 越界;
- 未初始化变量;
- 不被允许的 helper;
- 内核无法证明安全的控制流。
只有通过 Verifier,程序才能继续加载。
6.3 JIT 负责把字节码变成当前 CPU 更容易执行的机器指令
通过检查以后,系统可以把 BPF Bytecode JIT 成 CPU 本地机器码,从而提高执行效率。
这一阶段不需要一上来钻 Verifier 的所有细节。先把“编译 → 检查 → 执行”这条主线弄清楚就够了。
7. 第四阶段:系统学习几类常用 Probe
当 minimal 已经跑通,下一步就是把挂载点展开学习。
我建议顺序是:
tracepoint
↓
kprobe
↓
kretprobe
↓
uprobe
↓
uretprobe
原因很简单:minimal 本身就已经在使用 tracepoint,所以从它继续扩展最自然。
tracepoint
Linux 内核预先定义好的静态事件点,适合系统调用、调度、I/O 等监控。
kprobe / kretprobe
动态跟踪内核函数。
kprobe -> 函数入口
kretprobe -> 函数返回
需要函数参数、调用次数时可以用 kprobe;需要返回值或执行耗时时,再加入 kretprobe。
uprobe / uretprobe
作用对象换成用户态程序:
uprobe -> 用户函数入口
uretprobe -> 用户函数返回
可以用来跟踪自己写的程序、动态库甚至一些常见库函数。
这一阶段我单独整理了一篇五类探针笔记,重点是搞清“挂在哪里”和“什么时候触发”,而不是一上来追求写复杂工具。
8. 第五阶段:BPF Map——开始保存状态
只会“触发一个 eBPF 程序”还不够。很多实际需求都需要保存数据。
BPF Map 可以作为内核 eBPF 程序和用户态之间的重要数据结构:
内核 eBPF 程序
↕
BPF Map
↕
用户态程序
入门阶段我打算先掌握:
BPF_MAP_TYPE_HASH
BPF_MAP_TYPE_ARRAY
BPF_MAP_TYPE_PERCPU_ARRAY
最适合练手的小项目之一是:
统计每个 PID 调用
write()的次数。
逻辑非常直接:
write()
↓
eBPF
↓
获取 PID
↓
map[pid]++
↓
用户态读取 Map
最后可以输出:
PID COUNT
1234 50
2222 100
3456 17
做完这个小项目后,Map 的作用会比单纯背 API 清楚得多。
9. 第六阶段:Ring Buffer——把事件实时送到用户态
Map 更适合保存状态和统计结果,Ring Buffer 更适合实时事件流。
例如监控 openat():
openat()
↓
eBPF
↓
生成事件
PID
COMM
FILENAME
TIME
↓
Ring Buffer
↓
用户态实时接收
我目前给二者的区分是:
Map
-> 保存状态、计数、键值数据
Ring Buffer
-> 把事件连续发送给用户态
后面写系统调用监控器时,这两个机制通常会一起出现。
10. 第七阶段:再学习 BTF、vmlinux.h 和 CO-RE
我不打算太早碰 CO-RE 的细节。
在 tracepoint、Map、Ring Buffer 还没写熟之前,BTF 和 CO-RE 很容易学成一堆名词。
等前面的东西比较稳定后,再来看:
BTF
BTF 全称:
BPF Type Format
可以把它理解为内核类型信息的一种描述方式。
系统中如果存在:
ls /sys/kernel/btf/vmlinux
说明通常可以直接利用内核提供的 BTF 信息。
vmlinux.h
可以通过 bpftool 生成:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
eBPF 程序中再使用:
#include "vmlinux.h"
CO-RE
CO-RE:
Compile Once - Run Everywhere
它主要解决不同内核版本之间结构体布局可能变化的问题。
核心依赖关系可以先简单记成:
BTF
↓
vmlinux.h
↓
libbpf + CO-RE
11. 第八阶段:一定要做一个完整项目
基础学完以后,我不想继续只看零散 demo,而是准备做一个完整的系统调用监控器。
先监控:
openat
read
write
execve
用户态输出类似:
TIME PID COMM EVENT
10:20:01 1234 bash execve
10:20:03 1234 bash openat
10:20:03 1234 bash read
10:20:04 1234 bash write
项目结构可以很简单:
syscall_monitor/
├── monitor.bpf.c
├── monitor.c
├── monitor.h
└── Makefile
其中:
monitor.bpf.c -> 内核侧 eBPF 程序
monitor.c -> 用户态 loader / 事件处理
做到这里,前面学过的内容就能真正串起来:
SEC
tracepoint
kprobe
helper
BPF Map
Ring Buffer
Skeleton
libbpf
BTF
CO-RE
Verifier
这时我才认为自己真正完成了 eBPF 的入门阶段。
12. 第九阶段:再选深入方向
基础完成以后,再根据兴趣选方向。
Linux 性能分析
可以继续看:
CPU profiling
off-CPU
scheduler
block I/O
filesystem
memory
network
perf event
fentry / fexit
网络
可以继续看:
XDP
TC
socket
cgroup
大致位置关系:
网卡
↓
XDP
↓
Linux 网络协议栈
↓
TC
↓
Socket
sched_ext
我比较感兴趣的另一个方向是 sched_ext。
它允许使用 BPF 编写 Linux 调度策略,但我不打算把它当第一个 eBPF 项目,因为它前面至少还压着这些基础:
eBPF
BPF Map
BTF
CO-RE
libbpf
Linux 调度器
进程 / 线程
如果这些概念还没形成完整链路,直接研究 sched_ext 很容易只停留在“代码能跑,但原理没吃透”。
13. 我的实际练习顺序
理论学习之外,我给自己安排的练习大致是下面这样。
练习 1:跑通 minimal
目标:
能成功编译
能成功加载
能在 trace_pipe 中看到输出
练习 2:修改 tracepoint
把:
sys_enter_write
换成:
sys_enter_openat
观察什么操作会触发。
练习 3:监控 execve
执行:
ls
pwd
cat
观察不同命令启动时对应的 PID。
练习 4:增加进程名
输出:
PID
COMM
练习 5:加入 BPF Map
做:
PID -> syscall count
练习 6:加入 Ring Buffer
从内核实时发送事件,在用户态接收并打印。
练习 7:做一个简化版 execsnoop
监控系统中的进程执行事件。
这几个练习做完,比继续看十篇介绍文章更有价值。
14. 暂时不急着学什么
刚入门时,我会主动避开这些内容:
XDP 深入
复杂网络协议
LSM
高级 Verifier 技巧
大型 BCC 工具
sched_ext
复杂性能分析
不是这些内容不重要,而是它们都依赖更基础的东西。
如果下面这些还不熟:
tracepoint
BPF Map
Ring Buffer
BTF
CO-RE
越往后学,欠下的概念债越多。
15. 一个阶段性检查表
Linux / C
- 理解用户态和内核态
- 理解系统调用
- 理解进程、线程和 PID
- 理解文件描述符
- 熟悉指针和结构体
eBPF 基础
- 成功运行
minimal - 读懂
minimal.c - 读懂
minimal.bpf.c - 理解
SEC - 理解 helper
- 理解 Verifier
- 理解 JIT
Tracing
- tracepoint
- kprobe
- kretprobe
- uprobe
- uretprobe
数据交换
- HASH Map
- ARRAY Map
- PERCPU Map
- Ring Buffer
CO-RE
- BTF
vmlinux.h- CO-RE
项目
- syscall monitor
- exec monitor
- file monitor
- 简单性能监控工具
16. 当前最重要的一步
路线可以列得很长,但真正开始时没必要同时推进所有内容。
我目前认为最重要的一步仍然是:
把 libbpf-bootstrap/examples/c/minimal 跑通
然后按这个顺序继续:
1. 阅读 minimal.bpf.c
2. 阅读 minimal.c
3. 理解 tracepoint
4. 修改 sys_enter_write
5. 自己写第一个 eBPF 程序
先完成一个闭环,再扩展下一个概念。
对我来说,这比一开始就把 XDP、CO-RE、sched_ext 全部铺开,更适合真正把 eBPF 学扎实。