土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】libbpf 加载器工程:skeleton、auto-attach、map pinning 与 ring buffer 消费者

文章导航

分类入口
kernelebpf
标签入口
#ebpf#libbpf#skeleton#elf#auto-attach#ring-buffer#bpf-loader#bpftool#co-re

目录

你写了一个 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 字节码再加载到内核。这种”运行时编译”模式有三个根本问题:

  1. 编译依赖:目标机器上必须安装 clang/LLVM、内核头文件(通常几百 MB),这种依赖在生产环境中是不可接受的;
  2. 编译时间:每次加载都要编译,在频繁更新 BPF 程序的场景中引入明显延迟;
  3. 版本耦合:编译出的 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:

二、加载生命周期全景

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_typeexpected_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:

在 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.cbpf_program__set_type() 中通过对节名的字符串匹配来确定 prog_typeexpected_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_SYSCALL

3.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 结构体封装了四个核心函数:

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 的价值在于:

  1. 类型安全:如果 BPF 源码中 map 名改了,skel->maps.old_name 会产生编译错误,而不是运行时 NULL 返回带来的 segfault;
  2. 消除字符串匹配:map 和程序名不再是运行时字符串,而是编译期确定的符号;
  3. 简化的生命周期管理open_and_load() + attach() + destroy() 三个调用覆盖完整生命周期;
  4. auto-attachmyprog_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 模式:

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);

内部实现核心流程:

  1. ring_buffer__new()mmap() 映射 ring buffer 的共享内存区域,创建 struct ring_buffer_epoll(每个 ring buffer entry 一个 epoll 条目),注册 epoll fd;
  2. ring_buffer__poll():调用 epoll_wait() 等待 ring buffer 有数据可读;当 epoll 返回时,遍历所有就绪的 ring buffer entry,从 mmap 区域读取记录,调用用户的 sample_cb 回调;
  3. ring_buffer__consume():非阻塞版本,不等待直接消费已有数据;
  4. 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。


参考

  1. Linux 内核源码 tools/lib/bpf/libbpf.c(kernel 6.6):libbpf 加载器主文件
  2. Linux 内核源码 tools/lib/bpf/ringbuf.c:ring buffer 消费者实现
  3. Linux 内核源码 tools/lib/bpf/relo_core.c:CO-RE 重定位引擎
  4. Linux 内核源码 tools/bpf/bpftool/gen.c:bpftool gen skeleton 生成逻辑
  5. Linux 内核文档 Documentation/bpf/libbpf/:libbpf 用户态 API 文档
  6. Linux 内核文档 Documentation/bpf/ringbuf.rst:BPF ring buffer 设计文档

上一篇程序生命周期:load、attach、detach、pin 与引用计数(第 09 篇)

下一篇BTF 格式规范与内核类型系统(第 11 篇)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】实战:构建微型 eBPF 可观测 Agent

把 01--17 的知识串成一条实践线——从 libbpf skeleton 写第一个 BPF 程序、加载到内核、用 ring buffer 回传事件、用 CO-RE 实现跨内核版本兼容、map pinning 实现热升级、配上半自动化的 verifier 错误排障流程——构建一个麻雀虽小五脏俱全的 eBPF 可观测 Agent。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器

eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。


By .