2. eBPF程序生命周期与开发工具链
eBPF 程序不是一段“写完就能跑”的普通 C 代码。它从源码到内核中真正执行,要经过一条由用户态工具链和内核验证器共同把关的流水线:编写、编译、加载、验证、运行、卸载。理解这条流水线,比记住某个命令更重要,因为绝大多数“程序加载失败”“map 读不到数据”“挂载点没触发”的问题,都能在流水线的某个环节定位到原因。
本节先建立整体认识,再动手完成第一个 eBPF 程序。整体上,eBPF 程序的生命周期可以概括为六个阶段,它们之间的对象流转关系如下图所示。
图中左侧是用户态工具链,右侧是内核态。真正把两侧连接起来的是 bpf() 系统调用:加载时把编译好的 BPF 指令交给内核,验证器通过后程序才被允许运行。下面按阶段展开。
编写:一个最小可用的 XDP 程序
第一个程序选择 XDP 挂载点,因为它触发直观、依赖少。程序逻辑很简单:统计经过网卡的数据包数量,并把每个包放行。计数需要在内核态和用户态之间共享,因此要用到 BPF map。示例文件命名为 hello.bpf.c:
// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, __u64);
} pkt_count SEC(".maps");
SEC("xdp")
int hello_xdp(struct xdp_md *ctx)
{
__u32 key = 0;
__u64 *val = bpf_map_lookup_elem(&pkt_count, &key);
if (val)
__sync_fetch_and_add(val, 1);
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
这里有几个必须注意的点。SEC("xdp") 是段名约定,libbpf 和 bpftool 靠它识别程序类型和挂载点;SEC(".maps") 标记 map 定义;LICENSE 段不能省,内核会检查许可证,GPL 兼容的许可证才能使用部分辅助函数。返回值 XDP_PASS 表示把包交给协议栈继续处理,其他常见返回值还有 XDP_DROP(丢弃)和 XDP_TX(从原网卡发回)。
编译:clang/LLVM 生成 BPF 目标文件
eBPF 程序用 C 编写,但编译目标不是 x86 或 ARM,而是 BPF 指令集。这由 clang 的 -target bpf 完成,后端由 LLVM 提供。推荐加上 -g 生成 BTF 调试信息,libbpf 和 bpftool 依赖它来理解 map 和结构体布局:
clang -O2 -g -target bpf \
-D__TARGET_ARCH_x86 \
-I/usr/include/x86_64-linux-gnu \
-c hello.bpf.c -o hello.bpf.o
llvm-objdump -S hello.bpf.o # 查看反汇编,确认指令与段
编译成功后得到的是 ELF 格式的 hello.bpf.o,里面包含 xdp 段(程序指令)、.maps 段(map 元数据)、license 段和 BTF 信息。它还不能直接运行,需要加载器把它送进内核。
加载与验证:内核是最后一道关卡
加载方式有三种常见选择。用 bpftool 可以快速验证程序能否通过验证器:
bpftool prog load hello.bpf.o /sys/fs/bpf/hello \
type xdp
如果验证失败,命令会返回错误并打印 verifier 日志,日志里会指出具体指令行和拒绝原因,例如“invalid mem access”或“back-edge from insn 12 to 5”。这是排查问题最直接的入口。加载成功后,程序以 fd 形式存在,/sys/fs/bpf/hello 是 pin 到 bpffs 的路径,便于后续引用。
生产环境更常用 libbpf 的 skeleton 机制:bpftool gen skeleton hello.bpf.o > hello.skel.h 生成头文件,用户态程序通过 hello_bpf__open()、hello_bpf__load()、hello_bpf__attach() 三步完成加载和挂载,并可直接访问 skel->maps.pkt_count 读取 map。BCC 则走另一条路:它把 C 代码在运行时用 LLVM 编译并加载,适合快速脚本和观测,但部署时依赖较重。
运行与观测:确认程序真的在工作
把程序挂到网卡上并读取计数。假设网卡为 eth0:
ip link set dev eth0 xdp obj hello.bpf.o sec xdp
bpftool map dump name pkt_count
挂载后产生流量(例如 ping 或 curl),再执行 bpftool map dump,应看到 value 从 0 增长。若计数始终为 0,先确认挂载点是否正确、流量是否真的经过该网卡,再用 bpftool prog show 查看程序是否处于 attached 状态。用 ip link set dev eth0 xdp off 可以卸载。
卸载:引用计数决定释放时机
eBPF 对象由内核引用计数管理。程序 fd、map fd、link fd 每被引用一次,计数加一;关闭所有 fd 且没有其他引用(如 pin、attach)后,内核自动回收。因此卸载通常就是关闭 fd 或 detach 挂载点,不需要手动“删除程序”。用 bpftool 加载并 pin 的程序,需要 rm /sys/fs/bpf/hello 解除 pin 引用。
| 阶段 | 关键工具 | 产物/动作 | 常见失败原因 |
|---|---|---|---|
| 编写 | 编辑器 | hello.bpf.c | 缺少 LICENSE、段名错误 |
| 编译 | clang/LLVM | hello.bpf.o | 缺 -target bpf、头文件路径错 |
| 加载 | libbpf/bpftool/BCC | bpf() 系统调用 | 权限不足、内核不支持该程序类型 |
| 验证 | 内核 verifier | 通过或错误日志 | 越界访问、无界循环、未判空 |
| 运行 | JIT + hook | map 数据变化 | 挂载点未触发、返回值不当 |
| 卸载 | close/detach | 引用计数归零 | fd 泄漏、pin 未清理 |
把这条链路走通一次,后面学习 map 类型、辅助函数、CO-RE 和复杂挂载点时,遇到问题就能快速判断是编译、加载、验证还是运行阶段的问题。第一个程序的价值不在于功能,而在于建立对整条工具链的掌控感。