使用 libbpf + skeleton 编写 Futex 工具时,问题通常出现在四个阶段:对象没有正确生成、程序没有通过 verifier、程序没有 attach 到预期 hook,或者程序已经运行但 Map 中没有数据。几个阶段的表现很像,必须从前向后逐层排查。
一、从编译结果确认对象是否完整
clang -O2 -g -target bpf -c futexlat.bpf.c -o futexlat.bpf.o
bpftool gen skeleton futexlat.bpf.o > futexlat.skel.h
clang futexlat.c -lbpf -lelf -lz -o futexlat
CO-RE 依赖有效 BTF:
bpftool feature probe
bpftool btf dump file /sys/kernel/btf/vmlinux | head
出现 failed to find valid kernel BTF 或 struct_ops requires kernel CONFIG_DEBUG_INFO_BTF=y 时,应先检查内核配置和工具版本。
确认 Map 已进入 skeleton
grep -A3 hists futexlat.skel.h
预期存在:
struct {
struct bpf_map *hists;
} maps;
如果 maps.hists 不存在,应检查 .bpf.c 中的 Map 定义、section 和实际编译输入,而不是继续修改查询逻辑。
如果 BPF 侧已经调用 bpf_map_lookup_or_try_init(&hists, ...),并继续向 histp->comm 或直方图槽位写入,就能够确认该 Map 确实被程序引用,不应把问题简单归因于编译器删除未使用 Map。
二、分开检查 open、load 和 attach
obj = futexlat_bpf__open();
if (!obj) {
fprintf(stderr, "failed to open BPF object\n");
return 1;
}
err = futexlat_bpf__load(obj);
if (err) {
fprintf(stderr, "failed to load BPF object: %d\n", err);
goto cleanup;
}
err = futexlat_bpf__attach(obj);
if (err) {
fprintf(stderr, "failed to attach BPF programs: %d\n", err);
goto cleanup;
}
开启 libbpf_set_print(libbpf_print_fn),同时查看:
dmesg | tail -n 50
bpftool prog list | grep futex
sudo cat /sys/kernel/debug/tracing/trace_pipe
三、检查 Map fd、迭代逻辑和共享结构
int fd = bpf_map__fd(obj->maps.hists);
if (fd < 0) {
fprintf(stderr, "invalid hists map fd: %d\n", fd);
return -1;
}
EBADF 表示无效文件描述符,与“Map 为空”不是一回事。Map 为空时,bpf_map_get_next_key() 返回失败,用户态应结束迭代,不能继续拿未初始化的 next_key 做 lookup。
可以先写一个只打印 fd、调用一次 bpf_map_get_next_key() 的最小程序,暂时移除格式化输出和直方图遍历。若 fd 为正数但 lookup 仍失败,再检查 Map 类型、key/value 大小和传入指针,而不是继续排查 load 阶段。
__u32 key, next_key;
int err = bpf_map_get_next_key(fd, NULL, &next_key);
while (!err) {
struct hist hist = {};
if (bpf_map_lookup_elem(fd, &next_key, &hist)) {
fprintf(stderr, "lookup failed: %s\n", strerror(errno));
break;
}
key = next_key;
err = bpf_map_get_next_key(fd, &key, &next_key);
}
检查共享结构体
用户态和 BPF 侧必须使用一致的 key/value 布局,尤其要检查字段类型、顺序、数组长度和对齐。
#define MAX_SLOTS 64
struct hist {
char comm[16];
__u64 slots[MAX_SLOTS];
};
最好将共享结构放进同一个头文件。结构体不一致通常不会使 fd 无效,却会造成读取失败或数据错位。
四、程序已 attach,为什么仍然没有数据
Map 存在且 fd 正常,却没有任何 key 时,可以增加临时计数器:
volatile __u32 switch_counter;
static __always_inline int handle_switch(/* ... */)
{
__sync_fetch_and_add(&switch_counter, 1);
return 0;
}
用户态读取 obj->bss->switch_counter,或在 BPF 程序中临时加入 bpf_printk() 并观察 trace_pipe。
过滤条件和测试负载
如果目标 PID 没有进入 Futex 慢路径,Map 为空是正常结果。调试阶段可以暂时设置:
obj->rodata->targ_tgid = 0;
再制造可控负载:
sudo stress-ng --futex 2 --timeout 5s
grep futex_wait /proc/kallsyms
全局模式有数据、指定 PID 后无数据时,应优先检查 PID/TGID 语义和过滤条件;测试负载也无法触发时,再检查 attach 点和内核符号。
对于原始 futexlat,还要确认 handle_switch() 是否确实位于已 attach 的 futex_wait、futex_wake 或相应返回探针路径中。仅仅生成了 skeleton,并不能证明这些回调在当前内核和测试负载下会执行。
五、将 struct_ops 问题分开并形成排查顺序
LAVD 等 sched_ext 程序可能出现 failed to load: -22。BPF_STRUCT_OPS 依赖内核 BTF、对应内核功能和结构定义,不应与 Futex kprobe/tracepoint 问题混在一起。
当前实验笔记还记录过 errno 22 (EINVAL) 和 errno 14 (EFAULT)。这两个 errno 只能说明 loader 最终收到的错误,不能单独作为根因;发表前需要保留对应的完整 verifier 日志、内核版本和触发命令。
[Warn] libbpf: prog 'lavd_select_cpu': failed to load: -22
[Warn] libbpf: failed to load BPF skeleton 'bpf_bpf': -22
Error: Failed to load BPF program: Invalid argument (os error 22)
cat /sys/kernel/sched_ext/state /sys/kernel/sched_ext/*/ops 2>/dev/null
bpftool struct_ops list
bpftool btf dump file /sys/kernel/btf/vmlinux | head
推荐排查顺序
| 顺序 | 检查项 | 目标 |
|---|---|---|
| 1 | BTF、编译和 skeleton | 对象及 Map 已生成 |
| 2 | open |
skeleton 可以打开 |
| 3 | load 与 verifier |
程序进入内核 |
| 4 | attach |
hook 已建立 |
| 5 | Map fd | 用户态句柄有效 |
| 6 | 计数器和 trace_pipe |
程序实际执行 |
| 7 | PID/TGID 和测试负载 | 确实存在事件 |
| 8 | Map 迭代和结构体 | 数据读取正确 |
按阶段排查后,“程序没有加载”“probe 没触发”和“Map 本来为空”就不会再混为同一个问题。
参考资料
- David Calavera、Lorenzo Fontana,《Linux Observability with BPF》及中文版《Linux 内核观测技术 BPF》
- Brendan Gregg,《BPF Performance Tools》及中文版《BPF 之巅》
- eBPF 官方网站
- IO Visor 项目
- awesome-ebpf 资料索引
- Brendan Gregg 个人网站
- The BSD Packet Filter: A New Architecture for User-level Packet Capture