你写了一个 BPF 程序,它完美地通过了 verifier,用
bpftool prog load 也能加载。但当你把程序编译成
.o 文件,想让一个 C
用户态进程在生产环境中加载它时,问题来了:如何解析 ELF 中的
BPF 代码段和 map 定义?如何把 SEC("xdp")
这样的字符串映射到正确的 BPF_PROG_TYPE_XDP 和
attach 类型?如何把 map 的 bpf_map_def 翻译成
BPF_MAP_CREATE 系统调用?如何让用户态安全地读写
map 而不用手工拼接 key/value 为 hex 字符串?
这些问题没有一行 bpf(BPF_PROG_LOAD, ...)
能解决。你需要 libbpf。
本文从 libbpf 的加载生命周期出发,逐层拆解
bpf_object__open() /
bpf_object__load() / auto-attach
三个阶段的内部实现,讲清 skeleton 生成器在 ELF
解析之上的封装层,以及 ring buffer consumer 的 mmap
模式——覆盖从 .o 文件到运行中 BPF
程序的完整加载管线。源码基于 tools/lib/bpf/
随内核分发的 libbpf(kernel 6.6)。
一、libbpf 的角色与设计哲学
1.1 替代 bcc 的编译模型
在 libbpf 成为标准之前,BPF 程序的主流加载方式是 bcc(BPF Compiler Collection)。bcc 在运行时调用 clang/LLVM 将 BPF C 源码编译为 BPF 字节码再加载到内核。这种”运行时编译”模式有三个根本问题:
- 编译依赖:目标机器上必须安装 clang/LLVM、内核头文件(通常几百 MB),这种依赖在生产环境中是不可接受的;
- 编译时间:每次加载都要编译,在频繁更新 BPF 程序的场景中引入明显延迟;
- 版本耦合:编译出的 BPF 字节码与编译时的内核头文件绑定,更换内核版本通常需要重新编译。
libbpf 采用”编译一次,加载到处”(结合 CO-RE
实现跨内核版本兼容)的模式:BPF 程序在开发环境编译为
.o(ELF)文件,目标机器上只需安装 libbpf
共享库(几十 KB),通过解析 ELF
元数据完成加载。这与传统用户态程序的”compile → link →
load”模型一致,也是容器化、不可变基础设施时代的自然选择。
1.2 在工具链中的位置
libbpf 是 BPF 工具链的加载器层,位于 BPF ELF 目标和内核
bpf() syscall 之间:
flowchart TD
C["BPF C 源码"] --> CLANG["clang -target bpf -O2 -g"]
CLANG --> ELF["BPF ELF .o(含 .text, maps, .BTF, .BTF.ext, license 等节)"]
ELF --> BPFTOOL["bpftool gen skeleton → .skel.h"]
BPFTOOL --> SKEL["skeleton C 头文件(struct xxx_bpf)"]
ELF --> LIBBPF["libbpf 加载器"]
SKEL --> LIBBPF
LIBBPF --> SYS["bpf() syscall → 内核 verifier → JIT → attach"]
SYS --> RUN["运行中的 BPF 程序 + maps"]
libbpf 把以下复杂性封装成一套结构化的 API:
- ELF 节解析和 BPF 程序类型的映射(
SEC()注解系统) - Map 从
.maps节的结构体定义到BPF_MAP_CREATE系统调用的转换 - BTF 和 BTF.ext 节的加载(func_info、line_info、CO-RE 重定位)
- 各类型的程序 attach(perf_event_open、cgroup 路径绑定、kprobe/uprobe 注册等)
- 生命周期管理(引用计数、pin 到 bpffs、自动清理)
二、加载生命周期全景
libbpf 的完整加载生命周期分为四个阶段:
sequenceDiagram
participant User as 用户态程序
participant Libbpf as libbpf
participant Kernel as 内核
User->>Libbpf: bpf_object__open(file)
Note over Libbpf: 阶段 1:解析 ELF
Libbpf->>Libbpf: 解析 ELF 节表
Libbpf->>Libbpf: 识别 BPF 程序节(text, xdp, tc...)
Libbpf->>Libbpf: 识别 maps 节(.maps)
Libbpf->>Libbpf: 加载 BTF 和 BTF.ext 节
Libbpf->>Libbpf: 构建 CO-RE 重定位表
Libbpf-->>User: struct bpf_object
User->>Libbpf: bpf_object__load(obj)
Note over Libbpf: 阶段 2:加载到内核
Libbpf->>Kernel: BPF_MAP_CREATE(每个 map)
Kernel-->>Libbpf: map fd
Libbpf->>Libbpf: bpf_object__relocate_core()(修补 CO-RE 指令)
Libbpf->>Kernel: BPF_PROG_LOAD(每个程序,已修补)
Note over Kernel: verifier & JIT
Kernel-->>Libbpf: prog fd
Libbpf-->>User: 加载完成
User->>Libbpf: bpf_object__attach(obj)
Note over Libbpf: 阶段 3:挂载
Libbpf->>Kernel: BPF_LINK_CREATE / perf_event_open / ...
Kernel-->>Libbpf: bpf_link fd
Libbpf-->>User: 程序已挂载
User->>Libbpf: bpf_object__close(obj)
Note over Libbpf: 阶段 4:清理
Libbpf->>Kernel: close(fd) × N
Libbpf->>Libbpf: 释放 libbpf 内部结构
2.1 阶段
1:bpf_object__open() — ELF 解析
入口函数
bpf_object__open_file()(tools/lib/bpf/libbpf.c)是加载流程的起点。调用链如下:
bpf_object__open_file()
→ bpf_object__open(const char *path)
→ bpf_object_open_opts() → open(path, O_RDONLY)
→ bpf_object__elf_init(obj) // 初始化 obj->efile
→ 验证 ELF magic: ELFMAG "\x7fELF"
→ bpf_object__elf_collect(obj) // 收集所有 BPF 相关的 ELF 节
→ bpf_object__elf_fixup(obj) // 修正 cross-section 引用
→ bpf_object__elf_sort(obj) // 按 section 对 map/全局变量排序bpf_object__elf_collect() 是核心——它遍历 ELF
节表,将每个节分派到对应处理逻辑:
/* tools/lib/bpf/libbpf.c - bpf_object__elf_collect() 节分类逻辑
* 以 "license" 开头的节名 → 许可字符串
* 以 "maps" 或 "maps/" 开头的 → BPF map 定义
* 以 ".maps" 完全匹配的 → map 元数据(struct bpf_map_def)
* 以 ".text" 开头的 → 函数程序(可被 BPF_PSEUDO_CALL 调用)
* 其他任何 BPF 程序节 → 根据 SEC() 注解确定程序类型和 attach 类型
* ".BTF" → BTF 类型信息
* ".BTF.ext" → BTF 扩展(func_info + line_info + CO-RE 重定位)
*/ELF 解析完成后,struct bpf_object
中的关键字段状态:
| 字段 | 内容 | 来源 |
|---|---|---|
obj->efile.elf |
libelf Elf * 句柄 |
elf_begin() |
obj->efile.btf /
btf_ext |
BTF / BTF.ext 数据 | .BTF / .BTF.ext 节 |
obj->maps[] |
struct bpf_map * 数组 |
.maps 节 |
obj->programs[] |
struct bpf_program * 数组 |
BPF 程序节(text、xdp 等) |
obj->kconfig_map |
struct bpf_map * |
.kconfig 特殊 map |
2.2 阶段
2:bpf_object__load() — 批量加载到内核
bpf_object__load() 将
bpf_object 中的所有 map
和程序批量提交给内核。调用链:
bpf_object__load(obj)
→ bpf_object__create_maps(obj) // 创建所有 map
→ for each bpf_map: bpf_map_create()
→ bpf(BPF_MAP_CREATE, ...)
→ bpf_object__relocate(obj) // 处理非 CO-RE 重定位(map fd 等)
→ bpf_object__relocate_core(obj) // 应用 CO-RE 重定位,修补指令
→ bpf_object__load_progs(obj) // 加载所有程序
→ for each bpf_program: bpf_program__load()
→ bpf(BPF_PROG_LOAD, ...) // 提交已修补的字节码bpf_object__create_maps() 按依赖顺序创建
map。map 的创建顺序会影响程序加载——因为
BPF_LD_MAP_FD 指令中的 map fd 需要在
BPF_PROG_LOAD 之前就确定。libbpf 通过
bpf_map__reuse_fd() 支持 map 复用(例如通过
bpffs 上 pin 的 map
共享数据),这在程序热更新场景中至关重要:新程序加载后被
attach 到新 link,使用旧 map,数据不丢失。
bpf_object__load_progs() 遍历所有程序调用
bpf_program__load()。每个程序的加载参数通过
struct bpf_prog_load_opts 控制:
/* tools/lib/bpf/libbpf.c - bpf_program__load() 简化 */
static int bpf_program__load(struct bpf_program *prog, ...)
{
/* 设置 BPF_PROG_LOAD 属性 */
attr.prog_type = prog->type; /* 从 SEC() 注解推导 */
attr.expected_attach_type = prog->expected_attach_type;
attr.insns = prog->insns; /* BPF 字节码 */
attr.insn_cnt = prog->insns_cnt;
attr.license = obj->license; /* 来自 license 节 */
attr.log_level = opts->log_level; /* verifier 日志级别 */
attr.log_buf = log_buf;
attr.func_info = ...; /* 来自 BTF.ext */
attr.line_info = ...;
attr.fd_array = map_fds; /* map 文件描述符数组 */
attr.attach_btf_id = prog->attach_btf_id; /* fentry/fexit 等需要的 BTF ID */
prog->fd = sys_bpf_prog_load(&attr, ...);
}2.3 阶段 3:Attach — 挂载到内核钩子
bpf_object__attach()
遍历所有程序,根据每个程序的 prog_type 和
expected_attach_type 选择对应的 attach
函数。libbpf 在 bpf_program__attach()
内部维护了一个类型分发表:
/* tools/lib/bpf/libbpf.c - bpf_program__attach() 的类型分发 */
static struct bpf_link *bpf_program__attach(const struct bpf_program *prog)
{
switch (prog->type) {
case BPF_PROG_TYPE_TRACEPOINT:
return bpf_program__attach_tracepoint(prog);
case BPF_PROG_TYPE_KPROBE:
return bpf_program__attach_kprobe(prog);
case BPF_PROG_TYPE_PERF_EVENT:
return bpf_program__attach_perf_event(prog);
case BPF_PROG_TYPE_TRACING:
if (prog->expected_attach_type == BPF_TRACE_FENTRY ||
prog->expected_attach_type == BPF_TRACE_FEXIT)
return bpf_program__attach_trace(prog);
if (prog->expected_attach_type == BPF_TRACE_RAW_TP)
return bpf_program__attach_raw_tracepoint(prog);
break;
case BPF_PROG_TYPE_CGROUP_SKB:
return bpf_program__attach_cgroup(prog, ...);
case BPF_PROG_TYPE_XDP:
return bpf_program__attach_xdp(prog, ifindex);
case BPF_PROG_TYPE_SK_LOOKUP:
return bpf_program__attach_sk_lookup(prog, ...);
/* ... 其他类型 ... */
}
}每种 attach 函数最终都调用一个或多个 syscall:
- Tracepoint:
perf_event_open()创建 perf event,然后PERF_EVENT_IOC_SET_BPF绑定 BPF 程序 - Kprobe:
perf_event_open()创建 kprobe 事件,绑定 BPF 程序 - Fentry/Fexit:
bpf(BPF_LINK_CREATE, BPF_TRACING, ...)创建 trampoline link - XDP:
bpf(BPF_LINK_CREATE, BPF_XDP, ...)或传统 netlink 方式 - Cgroup:
bpf(BPF_PROG_ATTACH, cgroup_fd, ...) - LSM:
bpf(BPF_LINK_CREATE, BPF_LSM, ...)
在 6.1+ 内核上,所有 attach 操作最终收敛到
bpf_link 模型(BPF_LINK_CREATE
syscall),提供了统一的 attach/detach
生命周期,替代了各类型各自为政的 attach 方式。
2.4 阶段 4:Cleanup — 生命周期管理
bpf_object__close() 依次:detach
所有程序(关闭 bpf_link fd)、关闭所有 prog fd、关闭所有 map
fd、释放 libbpf 内部内存。内核通过 fd
引用计数机制,在最后一个 fd 关闭后才真正释放 BPF 程序和 map
资源——除非程序/ map 被 pin 到 bpffs 上。pinning 用
bpf_obj_pin(fd, path)(即
BPF_OBJ_PIN syscall),将 fd 的引用挂载到 bpffs
文件节点上,允许跨进程、跨生命周期的共享。
三、SEC() 注解系统:从字符串到程序类型
3.1 约定体系
libbpf 通过 ELF 节名映射 BPF 程序类型,依靠的是
SEC() 注解展开成的
__attribute__((section(...)))。libbpf 在
tools/lib/bpf/libbpf.c 的
bpf_program__set_type()
中通过对节名的字符串匹配来确定 prog_type 和
expected_attach_type。匹配逻辑集中在一组内部
section 名定义中:
/* tools/lib/bpf/libbpf.c - 节名到程序类型的映射(简化) */
"xdp" → BPF_PROG_TYPE_XDP
"tc" → BPF_PROG_TYPE_SCHED_CLS
"classifier" → BPF_PROG_TYPE_SCHED_CLS // 等价于 tc ingress
"action" → BPF_PROG_TYPE_SCHED_ACT // TC action
"socket" → BPF_PROG_TYPE_SOCKET_FILTER
"kprobe/" → BPF_PROG_TYPE_KPROBE // 前缀匹配
"uprobe/" → BPF_PROG_TYPE_KPROBE // 与 kprobe 共用 BPF_PROG_TYPE_KPROBE
"tracepoint/" → BPF_PROG_TYPE_TRACEPOINT
"tp/" → BPF_PROG_TYPE_TRACEPOINT // 缩写形式
"tp_btf/" → BPF_PROG_TYPE_TRACING, BPF_TRACE_RAW_TP
"fentry/" → BPF_PROG_TYPE_TRACING, BPF_TRACE_FENTRY
"fexit/" → BPF_PROG_TYPE_TRACING, BPF_TRACE_FEXIT
"iter/" → BPF_PROG_TYPE_TRACING, BPF_TRACE_ITER
"lsm/" → BPF_PROG_TYPE_LSM, BPF_LSM_MAC
"lsm.s/" → BPF_PROG_TYPE_LSM, BPF_LSM_CGROUP // sleepable LSM
"cgroup_skb/ingress" → BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_INGRESS
"cgroup_skb/egress" → BPF_PROG_TYPE_CGROUP_SKB, BPF_CGROUP_INET_EGRESS
"cgroup/sock" → BPF_PROG_TYPE_CGROUP_SOCK
"cgroup/setsockopt" → BPF_PROG_TYPE_CGROUP_SETSOCKOPT
"cgroup/getsockopt" → BPF_PROG_TYPE_CGROUP_GETSOCKOPT
"sockops" → BPF_PROG_TYPE_SOCK_OPS
"sk_skb/stream_parser" → BPF_PROG_TYPE_SK_SKB
"sk_skb/stream_verdict" → BPF_PROG_TYPE_SK_SKB
"sk_msg" → BPF_PROG_TYPE_SK_MSG
"struct_ops/" → BPF_PROG_TYPE_STRUCT_OPS
"syscall" → BPF_PROG_TYPE_SYSCALL3.2 SEC() 注解的完整示例
// SEC() 注解覆盖 BPF 网络/追踪/安全三大域
SEC("xdp") int xdp_filter(struct xdp_md *ctx) { ... }
SEC("tc") int tc_ingress(struct __sk_buff *skb) { ... }
SEC("tracepoint/syscalls/sys_enter_open") int tp_open(void *ctx) { ... }
SEC("tp_btf/sched_switch") int raw_tp_switch(u64 *ctx) { ... }
SEC("kprobe/tcp_connect") int kp_conn(struct pt_regs *ctx) { ... }
SEC("uprobe//bin/bash:readline") int up_readline(struct pt_regs *ctx) { ... }
SEC("fentry/tcp_connect") int fe_conn(struct pt_regs *ctx) { ... }
SEC("fexit/tcp_connect") int fx_conn(struct pt_regs *ctx) { ... }
SEC("lsm/bprm_check_security") int lsm_bprm(void *ctx) { ... }
SEC("cgroup_skb/ingress") int cg_ingress(struct __sk_buff *skb) { ... }
SEC("sockops") int sock_ops_handler(struct bpf_sock_ops *ctx) { ... }3.3 fentry/fexit 的 BTF ID 解析
fentry/fexit 程序需要知道目标内核函数的 BTF 函数
ID。libbpf 通过 libbpf_find_vmlinux_btf_id()
完成这个转换:
/* tools/lib/bpf/libbpf.c - libbpf_find_vmlinux_btf_id() 简化逻辑 */
int libbpf_find_vmlinux_btf_id(const char *name, enum bpf_attach_type attach_type)
{
struct btf *btf = btf__load_vmlinux_btf(); // 从 /sys/kernel/btf/vmlinux 加载
int id;
for (id = 1; id <= btf__type_cnt(btf); id++) {
const struct btf_type *t = btf__type_by_id(btf, id);
if (btf_kind(t) != BTF_KIND_FUNC)
continue;
if (strcmp(btf__name_by_offset(btf, t->name_off), name) == 0) {
btf__free(btf);
return id;
}
}
btf__free(btf);
return -ENOENT;
}这个函数遍历 vmlinux BTF 中的所有
BTF_KIND_FUNC
条目,按名称匹配。SEC("fentry/tcp_connect")
会让 libbpf 自动调用
libbpf_find_vmlinux_btf_id("tcp_connect", BPF_TRACE_FENTRY),将返回的
BTF ID 填入 bpf_prog_load_opts.attach_btf_id
传给内核。
四、Skeleton:类型安全的加载器封装
4.1 bpftool gen skeleton 的生成
bpftool 的 gen skeleton 命令将 BPF ELF .o
文件转换为一个 C 头文件:
bpftool gen skeleton myprog.bpf.o > myprog.skel.h生成的 skeleton 头文件包含一个
struct myprog_bpf 结构体,其成员与 BPF C
源码中的 map 和程序一一对应。以以下 BPF 程序为例:
// myprog.bpf.c
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 1024);
} conns SEC(".maps");
SEC("xdp")
int xdp_filter(struct xdp_md *ctx) { ... }bpftool 会在生成的 myprog.skel.h
中生成类似以下的结构:
/* 由 bpftool gen skeleton 自动生成 */
struct myprog_bpf {
struct bpf_object_skeleton *skeleton;
struct bpf_object *obj;
struct {
struct bpf_map *conns; // 与 BPF 源码中的 map 名称一致
} maps;
struct {
struct bpf_program *xdp_filter; // 与 BPF 源码中的函数名一致
} progs;
struct {
struct bpf_link *xdp_filter; // attach 后的 link
} links;
};每个 skeleton 结构体封装了四个核心函数:
myprog_bpf__open()— 创建 skeleton、打开 ELF、创建bpf_objectmyprog_bpf__load()— 调用bpf_object__load()加载到内核myprog_bpf__attach()— 遍历所有程序,调用bpf_program__attach()完成 auto-attachmyprog_bpf__destroy()— detach + close + 释放所有资源
4.2 为什么需要 Skeleton
不使用 skeleton 时,用户态代码需要通过字符串查找 map 和程序:
// 无 skeleton:字符串查找,无类型安全
struct bpf_map *map = bpf_object__find_map_by_name(obj, "conns");
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "xdp_filter");
// 操作 map 需要把 key/value 编码为 hex 字符串
int fd = bpf_map__fd(map);
int err = bpf_map_update_elem(fd, &key, &value, BPF_ANY);使用 skeleton 后:
// 有 skeleton:类型安全,编译期检查
struct myprog_bpf *skel = myprog_bpf__open_and_load();
struct bpf_map *map = skel->maps.conns; // 编译期检查成员名
struct bpf_program *prog = skel->progs.xdp_filter;
// map 操作仍通过 fd,但头文件通常会提供辅助宏来消除手工 fd 管理Skeleton 的价值在于:
- 类型安全:如果 BPF 源码中 map
名改了,
skel->maps.old_name会产生编译错误,而不是运行时NULL返回带来的 segfault; - 消除字符串匹配:map 和程序名不再是运行时字符串,而是编译期确定的符号;
- 简化的生命周期管理:
open_and_load()+attach()+destroy()三个调用覆盖完整生命周期; - auto-attach:
myprog_bpf__attach()自动根据每个程序的 SEC() 注解选择正确的 attach 类型和参数。
4.3 完整的 Skeleton 使用流程
#include "myprog.skel.h"
int main(int argc, char **argv)
{
struct myprog_bpf *skel;
int err;
/* 1. 打开 ELF + 加载到内核 */
skel = myprog_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* 2. Attach 到内核钩子 */
err = myprog_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
/* 3. 启动 ring buffer 消费者(见第五节) */
struct ring_buffer *rb = ring_buffer__new(
bpf_map__fd(skel->maps.events),
handle_event, /* 事件处理回调 */
NULL, /* 回调上下文 */
NULL /* opts */
);
if (!rb) {
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
printf("BPF program loaded and attached. Press Ctrl-C to exit.\n");
/* 4. 主循环:poll ring buffer */
while (!done) {
ring_buffer__poll(rb, 100 /* timeout ms */);
}
cleanup:
ring_buffer__free(rb);
myprog_bpf__destroy(skel);
return err;
}五、Map Pinning 与跨进程共享
5.1 pin 机制
BPF map
的生命周期默认与创建它的进程绑定——进程退出时,所有未 pin 的
map fd 被内核关闭,map 被释放。Map pinning 通过将此 map
持久化到 bpffs(bpf filesystem,通常在
/sys/fs/bpf/)来突破这个限制:
/* 将 map pin 到 bpffs */
err = bpf_map__pin(skel->maps.conns, "/sys/fs/bpf/myprog/conns");
/* 另一个进程可以复用已 pin 的 map */
map_fd = bpf_obj_get("/sys/fs/bpf/myprog/conns");5.2
BPF_F_PIN_GLOBAL_NS vs 每对象命名空间
libbpf 在 bpf_object__pin() 时支持两种 pin
模式:
- 全局命名空间(
BPF_F_PIN_GLOBAL_NS):所有 map 和程序共享一个 bpffs 目录,按类型分文件(maps/<name>、progs/<name>)。这是旧模式,容易出现名称冲突。 - 每对象命名空间(默认):每个
bpf_object使用独立的 bpffs 子目录(/sys/fs/bpf/<obj_name>/),map 和程序在该子目录下。libbpf 6.1+ 默认使用此模式。
5.3 热更新模式
Map pinning 为 BPF 程序热更新提供了基础——新版本程序通过
bpf_map__reuse_fd() 复用旧
map,数据不丢失:
/* 热更新:新程序复用旧 map */
struct myprog_bpf *new_skel;
/* 先打开旧程序所使用的 map */
int old_map_fd = bpf_obj_get("/sys/fs/bpf/myprog/conns");
/* 创建新 skeleton 但只做 open,不 load(因为要设置 map 复用) */
new_skel = myprog_bpf__open();
if (!new_skel) { ... }
/* 在 load 之前设置 map 复用 */
bpf_map__reuse_fd(new_skel->maps.conns, old_map_fd);
/* 加载新程序(将使用复用的 map) */
err = myprog_bpf__load(new_skel);
/* Attach 新程序 — 这会替换旧的 link */
err = myprog_bpf__attach(new_skel);
/* 销毁旧 skeleton(旧程序被 detach,但 map 数据保留) */
myprog_bpf__destroy(old_skel);注意 bpf_map__reuse_fd() 必须在
bpf_object__load() 之前调用——因为
BPF_PROG_LOAD 时需要知道 map 的
fd,内核才能将程序中的 BPF_LD_MAP_FD
伪指令转换为真正的 map 指针。
六、Ring Buffer Consumer:mmap 消费者模式
6.1 Ring buffer 与 perf buffer
BPF ring
buffer(BPF_MAP_TYPE_RINGBUF,5.8+)是 perf
buffer(BPF_MAP_TYPE_PERF_EVENT_ARRAY)的换代方案。相比
perf buffer,ring buffer 的核心优势:
| 特性 | Perf Buffer | Ring Buffer |
|---|---|---|
| 内存共享 | 每 CPU 独立缓冲区 | 共享 mmap 区域(每 CPU) |
| 记录顺序 | 全局乱序(per-CPU 有序) | 全局有序(可配置) |
| 自定义数据 | 封装在 perf_event_header 中 |
裸数据,无内核头部 |
| 内存效率 | 较高(独立分配) | 更高(共享 mmap,2 倍缓冲区可重写) |
| 唤醒语义 | perf_event_output() 每事件 |
批量 commit + 可选的
bpf_ringbuf_discard() |
6.2 libbpf ring buffer API
libbpf 提供的 ring buffer 消费者 API 封装了 mmap、epoll 和事件分发:
/* tools/lib/bpf/ringbuf.c */
struct ring_buffer *ring_buffer__new(int map_fd,
ring_buffer_sample_fn sample_cb,
void *ctx,
const struct ring_buffer_opts *opts);
int ring_buffer__poll(struct ring_buffer *rb, int timeout_ms);
void ring_buffer__free(struct ring_buffer *rb);内部实现核心流程:
ring_buffer__new():mmap()映射 ring buffer 的共享内存区域,创建struct ring_buffer_epoll(每个 ring buffer entry 一个 epoll 条目),注册 epoll fd;ring_buffer__poll():调用epoll_wait()等待 ring buffer 有数据可读;当 epoll 返回时,遍历所有就绪的 ring buffer entry,从 mmap 区域读取记录,调用用户的sample_cb回调;ring_buffer__consume():非阻塞版本,不等待直接消费已有数据;ring_buffer__free():释放 mmap、关闭 epoll fd、释放内部结构。
6.3 消费者示例
/* 用户态 ring buffer 事件处理回调 */
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
printf("pid=%d comm=%s duration_ns=%llu\n",
e->pid, e->comm, e->duration_ns);
return 0; /* 返回 0 表示继续消费 */
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct myprog_bpf *skel;
int err;
skel = myprog_bpf__open_and_load();
if (!skel)
return 1;
err = myprog_bpf__attach(skel);
if (err)
goto cleanup;
/* 创建 ring buffer 消费者 */
rb = ring_buffer__new(
bpf_map__fd(skel->maps.events), /* ringbuf map 的 fd */
handle_event, /* 每记录回调 */
NULL, /* 传递给回调的 ctx */
NULL /* opts(可用默认) */
);
if (!rb) {
fprintf(stderr, "Failed to create ring_buffer: %d\n", -errno);
goto cleanup;
}
/* 主循环 */
printf("Listening for events...\n");
while (!done) {
err = ring_buffer__poll(rb, 100 /* timeout ms */);
if (err == -EINTR) continue; /* 信号中断 */
if (err < 0) break; /* 实际错误 */
/* 也可以调用 ring_buffer__consume(rb) 做非阻塞消费 */
}
cleanup:
ring_buffer__free(rb);
myprog_bpf__destroy(skel);
return -err;
}Ring buffer consumer 的 mmap 模式是 libbpf 设计哲学的典型体现:将内核机制(mmap、epoll)封装成简单的回调式 API,使应用开发者不需要直接处理内存映射、同步原语和事件循环细节。
七、完整的加载器代码示例
以下是一个最小但完整的 BPF 用户态加载器,使用 skeleton 模式:
// loader.c — 基于 skeleton 的最小 BPF 加载器
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include "myprog.skel.h"
static volatile bool done = false;
static void sig_handler(int sig) { done = true; }
static int handle_event(void *ctx, void *data, size_t sz)
{
printf("Event: %zu bytes\n", sz);
return 0;
}
int main(int argc, char **argv)
{
struct myprog_bpf *skel;
struct ring_buffer *rb = NULL;
int err;
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* 打开 BPF 程序文件 */
skel = myprog_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* 可选:在 load 之前调整 map 大小 */
bpf_map__set_max_entries(skel->maps.conns, 8192);
/* 可选:设置 verifier log level(调试用) */
// bpf_object__set_log_level(skel->obj, 1);
/* 加载到内核 */
err = myprog_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load BPF skeleton: %d\n", err);
goto cleanup;
}
/* Attach 到内核钩子 */
err = myprog_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach: %d\n", err);
goto cleanup;
}
/* 创建 ring buffer consumer */
rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
if (!rb) {
err = -errno;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
printf("Running. Press Ctrl-C to stop.\n");
while (!done) {
ring_buffer__poll(rb, 100);
}
err = 0;
cleanup:
ring_buffer__free(rb);
myprog_bpf__destroy(skel);
return err;
}配套的 Makefile:
# Makefile
CLANG = clang
CFLAGS = -target bpf -O2 -g -Wall
LDFLAGS = -lbpf -lelf -lz
all: myprog.bpf.o loader
myprog.bpf.o: myprog.bpf.c
$(CLANG) $(CFLAGS) -c $< -o $@
myprog.skel.h: myprog.bpf.o
bpftool gen skeleton $< > $@
loader: loader.c myprog.skel.h
$(CC) -Wall -O2 -I. $< $(LDFLAGS) -o $@
clean:
rm -f myprog.bpf.o myprog.skel.h loader八、源码路径索引
本文涉及的 libbpf 源码均来自 Linux 6.6 主线:
| 组件 | 源码路径 | 关键函数/结构体 |
|---|---|---|
| ELF 解析 | tools/lib/bpf/libbpf.c |
bpf_object__elf_init(),
bpf_object__elf_collect() |
| Map 创建 | tools/lib/bpf/libbpf.c |
bpf_object__create_maps(),
bpf_map_create() |
| 程序加载 | tools/lib/bpf/libbpf.c |
bpf_program__load(),
bpf_object__load_progs() |
| CO-RE 重定位 | tools/lib/bpf/relo_core.c |
bpf_core_apply_relo() |
| Ring buffer consumer | tools/lib/bpf/ringbuf.c |
ring_buffer__new(),
ring_buffer__poll() |
| Skeleton 构建 | tools/bpf/bpftool/gen.c |
do_skeleton() |
| BTF 查找 | tools/lib/bpf/libbpf.c |
libbpf_find_vmlinux_btf_id() |
九、小结
libbpf 是 BPF 工具链的加载器层,将 ELF 文件中的 BPF
程序元数据转换为 bpf() 系统调用序列。三个关键
API ——
bpf_object__open()(解析)、bpf_object__load()(加载)、bpf_object__attach()(挂载)——构成了加载生命周期。Skeleton
在此基础上提供了类型安全的封装层,消除了字符串匹配查找和手工
FD 管理。Ring buffer consumer 用 mmap() + epoll
的组合实现了高效的用户态-内核态事件通道。这些 API
的组合是构建生产级 BPF Agent 的工程基础,第 20
篇将以本文为起点构建完整的微型可观测 Agent。
参考
- Linux 内核源码
tools/lib/bpf/libbpf.c(kernel 6.6):libbpf 加载器主文件 - Linux 内核源码
tools/lib/bpf/ringbuf.c:ring buffer 消费者实现 - Linux 内核源码
tools/lib/bpf/relo_core.c:CO-RE 重定位引擎 - Linux 内核源码
tools/bpf/bpftool/gen.c:bpftoolgen skeleton生成逻辑 - Linux 内核文档
Documentation/bpf/libbpf/:libbpf 用户态 API 文档 - Linux 内核文档
Documentation/bpf/ringbuf.rst:BPF ring buffer 设计文档
上一篇:程序生命周期:load、attach、detach、pin 与引用计数(第 09 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】实战:构建微型 eBPF 可观测 Agent
把 01--17 的知识串成一条实践线——从 libbpf skeleton 写第一个 BPF 程序、加载到内核、用 ring buffer 回传事件、用 CO-RE 实现跨内核版本兼容、map pinning 实现热升级、配上半自动化的 verifier 错误排障流程——构建一个麻雀虽小五脏俱全的 eBPF 可观测 Agent。
【eBPF 内核实现深度拆解】CO-RE 重定位引擎:libbpf 的运行时指令修补
从 clang 内置函数 __builtin_preserve_access_index 出发,追踪 BPF_CORE_READ 等宏如何生成 BTF.ext CO-RE 重定位记录,再到 libbpf 加载时 bpf_core_apply_relo() 根据目标内核 BTF 计算正确字段偏移量并修补 BPF 指令——可移植 BPF 的核心引擎。
【eBPF 内核实现深度拆解】BPF 编译工具链:clang 后端、目标文件布局与调试信息
从 clang -target bpf 的 LLVM BPF 后端出发,讲清 BPF 目标文件 .o 的 ELF section 布局约定、DWARF 到 BTF 的转换管线、bpftool gen 的工具链集成,以及 BPF 特有的 inline asm 语法。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。