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

【eBPF 内核实现深度拆解】程序生命周期:load、attach、detach、pin 与引用计数

文章导航

分类入口
kernelebpf
标签入口
#ebpf#bpf-prog#lifecycle#refcount#bpf-link#bpffs#linux-kernel

目录

一个 BPF 程序从编译产物到在内核中开始执行,再到最终被释放,经历四个阶段:加载(LOAD)、挂载(ATTACH)、运行(RUNNING)、卸载(UNLOAD)。每个阶段都有明确的 API 和内核数据结构支撑,但理解这些 API 只是表面——真正决定程序何时停止执行、内存何时被回收的,是引用计数模型。

引用计数出问题是生产中最常见的 eBPF 故障类型。程序和 map 因为引用计数泄露而永远不被释放(“僵尸 BPF 程序”);程序因为 FD 被关闭而意外 detach(但开发者的本意是持久化);pin 到 bpffs 的程序在重启后还存在,但开发者忘了这一点。这些坑的根因,都在生命周期管理的实现细节里。

本文跟踪一个 BPF 程序从 bpf(BPF_PROG_LOAD) 开始直到 bpf_prog_free() 结束的完整生命周期,拆解 aux->refcnt 引用计数模型——用户态 FD、attach、pin、prog_array 等不同来源如何共同决定程序何时释放——以及各种 attach 类型的生命周期差异。

源码基准:Linux 6.8,路径 kernel/bpf/syscall.ckernel/bpf/core.ckernel/bpf/inode.cinclude/linux/bpf.h

一、全生命周期概览

一个 BPF 程序的生命周期可以展开为以下状态:

stateDiagram-v2
    [*] --> CREATED: bpf(BPF_PROG_LOAD)
    CREATED --> ATTACHED: attach (bpf_link / netlink / raw_tp)
    ATTACHED --> DETACHED: detach 或 bpf_link FD close
    DETACHED --> CREATED: 仍有 FD 引用 → 可重新 attach
    DETACHED --> [*]: 引用计数归零 → bpf_prog_free()
    ATTACHED --> [*]: FD close + detach → 引用计数归零
    CREATED --> [*]: FD close → 引用计数归零
    CREATED --> PINNED: BPF_OBJ_PIN → bpffs
    PINNED --> CREATED: BPF_OBJ_GET → 获取新 FD
    PINNED --> ATTACHED: attach 已 pin 的程序
    ATTACHED --> ATTACHED_PINNED: pin bpf_link → bpffs
    ATTACHED_PINNED --> ATTACHED: 仍有 kernel ref(s)
    ATTACHED_PINNED --> DETACHED: unpin + no other refs

关键点:程序对象在内核中的存在,完全由引用计数决定——与 attach 状态无关,与 FD 是否打开无关,与用户态进程是否存活无关。只要引用计数不为零,程序就继续存在。

二、struct bpf_prog 的内核表示

2.1 核心结构

/* include/linux/bpf.h — Linux 6.8 */
struct bpf_prog {
    u16                 pages;          /* 占用的页面数 */
    u16                 jited:1,        /* 是否已 JIT 编译 */
                        jit_requested:1,/* 是否请求了 JIT */
                        gpl_compatible:1,/* 是否 GPL 兼容 */
                        cb_access:1,    /* 是否访问了 cb 字段 */
                        dst_needed:1,   /* 是否需要 dst 缓存 */
                        blinded:1,      /* 是否做了 constant blinding */
                        is_func:1,      /* 是否是一个子程序 */
                        kprobe_override:1,/* 是否覆写了 kprobe */
                        has_callchain_buf:1,/* 是否有 callchain buffer */
                        enforce_expected_attach_type:1,
                        call_get_stack:1,/* 是否调用 get_stack */
                        sleepable:1;    /* 是否可休眠 */
    enum bpf_prog_type  type;           /* 程序类型:XDP / KPROBE / ... */
    enum bpf_attach_type expected_attach_type;
    u32                 len;            /* BPF 指令数量 */
    u32                 jited_len;      /* JIT 后本机码长度 */
    u8                  tag[BPF_TAG_SIZE];
    struct bpf_prog_stats __percpu *stats;
    int __percpu        *active;        /* 各 CPU 上的活跃执行计数 */
    unsigned int        (*bpf_func)(const void *ctx,
                                     const struct bpf_insn *insn);
    struct bpf_prog_aux *aux;           /* 辅助元数据 */
    /* JIT 后的本机码和数据在此 */
    struct sock_filter  *orig_prog;     /* 原始 cBPF(如有) */
    union {
        struct bpf_insn  insnsi[];      /* BPF 字节码 */
        struct bpf_insn  insns[];
    };
};

/* include/linux/bpf.h — Linux 6.8 */
struct bpf_prog_aux {
    atomic64_t          refcnt;         /* 内核引用计数 */
    u32                 used_map_cnt;   /* 使用的 map 数量 */
    u32                 used_btf_cnt;   /* 使用的 BTF 数量 */
    u32                 max_ctx_offset;
    u32                 max_pkt_offset;
    u32                 max_tp_access;
    u32                 stack_depth;    /* 栈深度 */
    u32                 id;             /* 程序 ID */
    u32                 func_cnt;       /* 子程序数量 */
    u32                 func_idx;       /* 当前函数在 prog array 中的索引 */
    struct bpf_map      **used_maps;    /* 程序使用的 map 列表 */
    struct btf          *btf;
    struct btf_mod_pair *used_btfs;
    u32                 btf_id;
    u32                 attach_btf_id;
    struct bpf_prog     *linked_prog;   /* bpf_link 关联的程序 */
    bool                verifier_zext;  /* 是否做了 zero-extend 优化 */
    bool                offload_requested;
    bool                sleepable;
    bool                tail_call_reachable;
    bool                xdp_has_frags;
    bool                use_after_unreachable;
    bool                call_get_stack;
    enum bpf_prog_type  saved_type;
    struct bpf_prog_offload *offload;
    union {
        struct work_struct work;        /* RCU 延迟释放 */
        struct rcu_head rcu;
    };
    struct user_struct  *user;          /* 内存记账 */
    u64                 load_time;      /* 加载时间 */
    u64                 duration_ns;    /* 验证耗时(ns) */
    u32                 verified_insns; /* 已验证的指令数 */
    char                name[BPF_OBJ_NAME_LEN];
    ...
    struct bpf_prog     *prog;          /* 指向包含的 bpf_prog */
    ...
    struct mutex        used_maps_mutex;
    struct mutex        used_btf_mutex;
    struct bpf_prog_ops *ops;           /* 程序类型操作 */
    struct btf_mod_pair *used_btf_mods;
    ...
};

2.2 引用计数:aux->refcnt

Linux 6.8 中,BPF 程序的生命周期由 struct bpf_prog_aux 里的单一引用计数 aux->refcnt 统一管理。上面 2.1 节的 struct bpf_prog 摘录中没有独立的 prog->refcnt 字段——所有”谁还持有这个程序”的信息都汇聚到 aux->refcnt 上,通过 bpf_prog_inc() / bpf_prog_put() 操作:

/* include/linux/bpf.h — 引用计数操作 */
static inline struct bpf_prog *bpf_prog_inc(struct bpf_prog *prog)
{
    atomic64_inc(&prog->aux->refcnt);
    return prog;
}

static inline void bpf_prog_inc_not_zero(struct bpf_prog *prog)
{
    atomic64_inc_not_zero(&prog->aux->refcnt);
}

static inline void bpf_prog_put(struct bpf_prog *prog)
{
    if (atomic64_dec_and_test(&prog->aux->refcnt))
        __bpf_prog_put(prog);
}

用户态 FD 也是引用来源之一。 BPF_PROG_LOAD 成功后,bpf_prog_new_fd() 创建一个 anon_inode 类型的 struct filefilp->private_data 指向 bpf_prog。每个指向该程序的 FD(包括 dup()BPF_OBJ_GET 从 bpffs 获取的新 FD)都对应一次 bpf_prog_inc()close(fd) 走 VFS 的 bpf_prog_release(),内部调用 bpf_prog_put() 释放这份引用:

/* kernel/bpf/syscall.c — bpf_prog_release()(close(fd) 的调用路径) */
static int bpf_prog_release(struct inode *inode, struct file *filp)
{
    struct bpf_prog *prog = filp->private_data;

    bpf_prog_put(prog);
    return 0;
}

attach、pin、prog_array 等内核实体同样通过 bpf_prog_inc() 持有引用。 下表 2.3 节列出了各来源;它们与用户态 FD 引用的是同一个 aux->refcnt,只是释放路径不同。

为什么 close(fd) ≠ detach ≠ free? 三者操作的是不同层面的资源,但共享同一个引用计数:

操作 实际效果 程序是否还在内核中
close(fd) 释放一份 FD 对应的 aux->refcnt 引用 若 attach / pin / prog_array 等仍持有引用 → 仍在
detach 从 hook 上移除程序,释放 attach 持有的 aux->refcnt 引用 若用户态仍持有 FD 或其他引用 → 仍在,只是不再执行
free(aux->refcnt 归零) __bpf_prog_put() 回收内存 不再存在

典型场景:用户态加载程序、attach 到 hook,然后进程退出关闭 FD。此时 FD 引用归零,但 attach 仍持有引用——程序继续在内核 hook 上运行。后续管理员 detach 或销毁持有引用的 cgroup/netdev,最后一份引用释放,程序才被回收。

反过来也成立:先 detach 但用户态仍持有 prog FD,程序对象仍在内存中,可以重新 attach。

2.3 引用的各种来源

aux->refcnt 增加的场景:

引用来源 函数入口 释放时机
用户态 FD(anon_inode file) bpf_prog_new_fd() / bpf_prog_get()bpf_prog_inc() close(fd)bpf_prog_release()bpf_prog_put()
attach 到 cgroup cgroup_bpf_attach() detach 或 cgroup 销毁
attach 到 TC/XDP (netlink) __bpf_prog_inc() netlink detach 或 netdev 销毁
attach 到 bpf_link bpf_link_create() link FD close 或 BPF_LINK_DETACH
tracepoint/kprobe (perf_event) perf_event_open() + ioctl perf_event close
尾调用程序数组 (PROG_ARRAY) bpf_prog_array_update() 从数组移除
map-in-map 的内层程序 bpf_map_fd_put_ptr() 内层 map 释放
struct_ops map bpf_struct_ops_map_link_create() 注销

三、阶段一:LOAD — BPF_PROG_LOAD 的内核路径

bpf(BPF_PROG_LOAD, &attr, size) 是程序生命周期的起点。

/* kernel/bpf/syscall.c — bpf_prog_load() 简化 */
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr)
{
    struct bpf_prog *prog;
    int err;

    /* 1. 权限检查 */
    if (!bpf_capable())
        return -EPERM;

    /* 2. 分配 bpf_prog 结构体 */
    prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER);
    if (!prog)
        return -ENOMEM;

    /* 3. 从用户态复制字节码 */
    prog->len = attr->insn_cnt;
    err = copy_from_bpfptr(prog->insns, make_bpfptr(attr->insns, uattr.is_kernel),
                            bpf_prog_insn_size(prog));
    if (err)
        goto free_prog;

    prog->type = attr->prog_type;
    prog->expected_attach_type = attr->expected_attach_type;

    /* 4. 核心:verifier 验证 */
    err = bpf_check(&prog, attr, uattr);
    if (err < 0)
        goto free_prog;

    /* 5. 授权检查(GPL 兼容性等) */
    err = bpf_prog_alloc_id(prog);
    if (err)
        goto free_prog;

    /* 6. JIT 编译(如果启用) */
    prog = bpf_prog_select_runtime(prog, &err);
    if (err < 0)
        goto free_prog;

    /* 7. 分配 FD 并返回给用户态 */
    err = bpf_prog_new_fd(prog);
    if (err < 0)
        goto free_prog;

    /* 8. 内存审计 */
    bpf_prog_kallsyms_add(prog);      /* 加入 kallsyms */
    bpf_prog_charge_memlock(prog);

    return err;

free_prog:
    bpf_prog_free(prog);
    return err;
}

关键步骤拆解:

步骤 1-3:权限检查、内存分配、字节码复制。这三步是标准的 resource acquisition,出错时直接释放。

步骤 4bpf_check() 执行 verifier。这是整个加载流程中最耗时也最复杂的部分(参见第 02、03 篇)。如果 verifier 拒绝,程序不会产生任何副作用——没有分配 ID,没有获得引用,直接 goto free_prog

步骤 5bpf_prog_alloc_id() 分配唯一的 prog->aux->id。此时程序获得内核身份,可以被 bpftool 看到。

步骤 6bpf_prog_select_runtime() 决定执行模式。如果 JIT 启用,调用 bpf_int_jit_compile() 将 BPF 字节码编译为本机码。prog->bpf_func 将被设置为: - JIT 编译的 x86/ARM 代码入口(prog->jited = true) - 解释器入口(___bpf_prog_run())(prog->jited = false

/* kernel/bpf/core.c — bpf_prog_select_runtime() 简化 */
struct bpf_prog *bpf_prog_select_runtime(struct bpf_prog *prog, int *err)
{
    if (prog->jit_requested) {
        prog = bpf_int_jit_compile(prog);
        if (!prog->jited) {
            /* JIT 失败 → 回退到解释器 */
            if (!bpf_prog_is_offloaded(prog->aux))
                *err = -ENOTSUPP;
            goto out;
        }
    } else {
        /* 使用解释器 */
        *err = bpf_prog_alloc_jited_linfo(prog);
        if (*err)
            goto out;
    }

    /* 根据是否 JIT 选择入口函数 */
    if (!prog->jited)
        prog->bpf_func = (void *)___bpf_prog_run;

out:
    return prog;
}

步骤 7bpf_prog_new_fd() 创建一个 anon_inode 类型的 struct filefilp->private_data 指向 bpf_prog,并返回 FD。FD 是 aux->refcnt 的引用来源之一——close(fd)bpf_prog_release() 调用 bpf_prog_put() 释放这份引用。若此时 attach、pin 等仍持有其他引用,程序对象继续存活。

四、阶段二:ATTACH — 各种 attach 类型的机制差异

BPF 程序加载后不会自动执行——必须先 attach 到一个 hook 上。不同程序类型使用不同的 attach 机制。

4.1 bpf_link:持久化 attach 的标准接口

Linux 5.7 引入的 bpf_link 是 attach 的标准接口。bpf_link 是一个独立的内核对象,将程序和 hook 绑在一起。

/* include/linux/bpf.h — struct bpf_link */
struct bpf_link {
    atomic64_t          refcnt;
    const struct bpf_link_ops *ops;
    struct bpf_prog     *prog;
    struct work_struct  work;
    struct file         *file;           /* 关联的 FD */
    struct idr          *idr;
    int                 id;
};

bpf_link 的生命周期独立于程序:一个程序可以没有 link(未 attach),一个 link 必须有一个程序。link FD 关闭时触发 detach(除非 link 被 pin 到 bpffs)。

/* kernel/bpf/syscall.c — bpf_link_create() 简化 */
static int bpf_link_create(union bpf_attr *attr)
{
    struct bpf_prog *prog;
    int ret;

    /* 1. 通过 FD 获取 prog */
    prog = bpf_prog_get(attr->link_create.prog_fd);
    if (IS_ERR(prog))
        return PTR_ERR(prog);

    /* 2. 根据 attach_type 选择创建逻辑 */
    switch (prog->type) {
    case BPF_PROG_TYPE_TRACING:
        ret = bpf_tracing_link_create(prog, attr);
        break;
    case BPF_PROG_TYPE_CGROUP_SKB:
    case BPF_PROG_TYPE_CGROUP_SOCK:
    case BPF_PROG_TYPE_CGROUP_SOCK_ADDR:
        ret = bpf_cgroup_link_create(prog, attr);
        break;
    case BPF_PROG_TYPE_LSM:
        ret = bpf_lsm_link_create(prog, attr);
        break;
    ...
    }

    /* 3. 成功时 link 持有 prog 的引用 */
    bpf_prog_put(prog);  /* 释放我们通过 bpf_prog_get 获得的临时引用 */
    return ret;
}

bpf_link 的关键特性:link FD 关闭时自动 detach。这提供了 RAII 风格的资源管理:

/* kernel/bpf/syscall.c — bpf_link_release() */
static void bpf_link_release(struct bpf_link *link)
{
    if (link->ops->release)
        link->ops->release(link);  /* detach 程序 */
}

但如果 link 被 pin 到 bpffs,关闭 FD 不会触发 release——pin 持有额外的引用。

4.2 raw_tracepoint:perf_event 的隐式生命周期

bpf_raw_tracepoint_open() 返回一个 FD,该 FD 内部是一个 perf_event

/* kernel/bpf/syscall.c — bpf_raw_tracepoint_open() 简化 */
static int bpf_raw_tracepoint_open(union bpf_attr *attr)
{
    struct bpf_prog *prog;
    struct bpf_raw_event_map *btp;
    int fd;

    /* 解析 tracepoint 名称 → bpf_raw_event_map */
    btp = bpf_get_raw_tracepoint(attr->raw_tracepoint.name);
    if (!btp)
        return -ENOENT;

    /* 获取 prog */
    prog = bpf_prog_get(attr->raw_tracepoint.prog_fd);
    if (IS_ERR(prog))
        return PTR_ERR(prog);

    /* 创建 perf_event,其 private_data 指向 prog */
    fd = bpf_raw_tp_event_open(btp, prog);
    /* prog 的引用由 perf_event 持有 */

    bpf_prog_put(prog);
    return fd;
}

这里的关键设计:perf_event 内部持有 prog 的引用(通过 bpf_prog_inc())。当用户态关闭 raw_tracepoint_open 返回的 FD 时,perf_eventrelease 路径调用 bpf_prog_put(),然后 detach 程序。

这意味着 raw tracepoint 的 attach 生命周期完全绑定在 FD 上——FD 关闭即 detach,没有持久化的机制。如果需要持久化,可以使用 bpf_link API(如果对应 tracepoint 支持)。

4.3 cgroup BPF:层级存储与继承

cgroup BPF 程序存储在 cgroup 结构体的 bpf 字段中:

/* include/linux/cgroup-defs.h — cgroup BPF 存储 */
struct cgroup_bpf {
    struct bpf_prog_array __rcu *effective[BPF_CGROUP_MAX_TYPE];
    struct bpf_prog_array __rcu *progs[BPF_CGROUP_MAX_TYPE];  /* 本层的程序 */
    struct bpf_prog_list *prog_lists[BPF_CGROUP_MAX_TYPE];
    ...
};

cgroup BPF 程序通过 BPF_PROG_ATTACH syscall 挂载(内核 5.7 之前),或通过 bpf_link API(5.7+)。程序存储在 cgroup 的 progs[type] 中,并传播到所有子 cgroup 的 effective[type] 中。

/* kernel/bpf/cgroup.c — cgroup_bpf_attach() 简化 */
static int cgroup_bpf_attach(struct cgroup *cgrp,
                              struct bpf_prog *prog,
                              enum bpf_attach_type type,
                              u32 flags)
{
    struct bpf_prog_list *pl;

    pl = kzalloc(sizeof(*pl), GFP_KERNEL);
    pl->prog = prog;
    bpf_prog_inc(prog);  /* cgroup 持有 prog 引用 */

    /* 添加到 cgroup 的程序列表并重新计算 effective */
    err = update_effective_progs(cgrp, type, pl, ...);

    return err;
}

cgroup BPF 的 detach:cgroup 被删除时,cgroup_bpf_release() 释放所有存储的程序。

XDP 和 TC BPF 通过 netlink 消息 attach:

/* net/core/dev.c — dev_xdp_attach() 简化 */
static int dev_xdp_attach(struct net_device *dev,
                           struct netlink_ext_ack *extack,
                           struct bpf_xdp_link *link,
                           struct bpf_prog *new_prog,
                           struct bpf_prog *old_prog,
                           u32 flags)
{
    struct bpf_prog *cur_prog;

    /* 使用 RCU 替换设备上的 BPF 程序 */
    cur_prog = rtnl_dereference(dev->xdp_prog);
    if (flags & XDP_FLAGS_REPLACE) {
        if (cur_prog != old_prog)
            return -EINVAL;
    }

    bpf_prog_inc(new_prog);  /* netdev 持有引用 */
    rcu_assign_pointer(dev->xdp_prog, new_prog);
    if (cur_prog)
        bpf_prog_put(cur_prog);  /* 释放旧程序 */

    return 0;
}

关键差异:XDP 程序的生命周期与 net_device 绑定,而不是与任何用户态 FD 绑定。关闭创建这个 attach 的进程的 FD 不会导致 detach——内核 net_device 结构体本身持有程序的引用。只有显式的 netlink detach 或设备删除才会释放程序。

4.5 Attach 类型对比

Attach 方式 接口 引用持有人 FD close 是否 detach 是否支持 pin 持久化
bpf_link BPF_LINK_CREATE bpf_link 对象 是(除非 pin) 是(pin link)
raw tracepoint bpf_raw_tracepoint_open() perf_event
cgroup (旧) BPF_PROG_ATTACH cgroup 结构体 否(绑定在 cgroup 上)
XDP netlink(ip link net_device 结构体 否(绑定在设备上)
TC BPF netlink(tc tcf_proto 等 TC 对象 否(绑定在 TC 过滤器上)
struct_ops BPF_MAP_UPDATE_ELEM + link struct_ops map 否(由 map 间接引用) 是(pin map)

五、BPFFS:bpffs 持久化与对象共享

BPF 文件系统(bpffs,通常挂载在 /sys/fs/bpf)是 BPF 对象(prog、map、link)的唯一持久化机制。

5.1 BPF_OBJ_PIN:将对象绑定到文件系统路径

/* kernel/bpf/inode.c — bpf_obj_pin() 简化 */
static int bpf_obj_pin(const char __user *pathname, struct fd fd)
{
    struct inode *inode;

    /* 1. 在 bpffs 中创建路径(含中间目录) */
    inode = bpf_mkobj_ops(path.dentry);

    /* 2. 将对象关联到 inode */
    inode->i_private = bpf_obj->raw;  /* bpf_prog * / bpf_map * / bpf_link * */

    /* 3. 增加对象的引用计数 */
    atomic64_inc(&bpf_obj->refcnt);
    ...
}

Pin 操作之后: - bpf_prog(或 bpf_map/bpf_link)与 bpffs inode 关联 - bpffs inode 持有一个引用(aux->refcnt 增加) - 用户态可以关闭原始 FD,程序仍存在 - 路径上的文件代表了程序在内核中的存在

5.2 BPF_OBJ_GET:从路径获取对象

/* kernel/bpf/inode.c — bpf_obj_get() 简化 */
static int bpf_obj_get(const char __user *pathname, int flags)
{
    /* 1. 解析 bpffs 路径 */
    path = user_path_at(AT_FDCWD, pathname, LOOKUP_FOLLOW);

    /* 2. 检查 inode 的 BPF 对象类型 */
    raw = bpf_any_get(path.dentry->d_inode->i_private, &type);

    /* 3. 根据类型分配 FD */
    switch (type) {
    case BPF_TYPE_PROG:
        fd = bpf_prog_new_fd(raw);
        break;
    case BPF_TYPE_MAP:
        fd = bpf_map_new_fd(raw, flags);
        break;
    case BPF_TYPE_LINK:
        fd = bpf_link_new_fd(raw);
        break;
    }

    return fd;
}

BPF_OBJ_GET 只针对已 pinned 的对象工作。它返回一个新的 FD,指向内核中已存在的那块数据——不是复制,是指向同一个对象。

5.3 Pin 的典型用法

跨进程共享 map:进程 A pin map 到 bpffs,进程 B 通过 BPF_OBJ_GET 获得同一个 map 的 FD,然后可以读写。

# 进程 A
bpftool map pin id 123 /sys/fs/bpf/shared_config

# 进程 B
bpftool map show pinned /sys/fs/bpf/shared_config

持久化程序

# Pin 一个程序(程序在进程退出后继续运行)
bpftool prog pin id 456 /sys/fs/bpf/my_xdp_prog

# 稍后获取并重新操作它
bpftool prog show pinned /sys/fs/bpf/my_xdp_prog

bpf_link 是最灵活的 attach 模型,因为 link 本身可以被 pin:

用户态:
  fd = bpf(BPF_LINK_CREATE, ...)       // 创建 link,自动 attach
  bpf(BPF_OBJ_PIN, "/sys/fs/bpf/my_link", fd)  // pin link
  close(fd)  // 关闭 FD,但 link 因为被 pin 而不会 detach

  // 程序持续运行,独立于用户态进程

稍后:
  fd = bpf(BPF_OBJ_GET, "/sys/fs/bpf/my_link")  // 获取 link FD
  close(fd)  // link 引用计数归零 → detach → 程序停止

这个模式实现了一个经典的”fire-and-forget”部署模式:加载 BPF 程序、创建 link、pin link、退出进程。程序保持运行直到管理员显式 unpin 并关闭 link。

六、阶段三与四:DETACH 与 UNLOAD

6.1 Detach 的各种路径

程序从 hook 上移除,但仍在内存中:

Attach 类型 Detach 方法 内核路径
bpf_link close(link_fd)BPF_LINK_DETACH bpf_link_release()link->ops->release()
cgroup (旧) BPF_PROG_DETACH cgroup_bpf_detach()
XDP ip link set dev eth0 xdp off dev_xdp_uninstall()
TC BPF tc filter del dev eth0 ... tcf_proto_destroy()
raw tracepoint close(fd) perf_event_release_kernel()
struct_ops unpin + map FD close bpf_struct_ops_map_put()

6.2 最终释放:bpf_prog_free

aux->refcnt 通过 bpf_prog_put() 递减至 0 时,调用 __bpf_prog_put() 回收程序内存:

/* kernel/bpf/core.c — __bpf_prog_put() 简化 */
static void __bpf_prog_put(struct bpf_prog *prog)
{
    /* 释放子程序 */
    bpf_prog_put_subprogs(prog);

    /* 释放 JIT 镜像 */
    bpf_jit_free_exec(prog);

    /* 释放 map 引用 */
    for (i = 0; i < prog->aux->used_map_cnt; i++)
        bpf_map_put(prog->aux->used_maps[i]);

    /* BTF 引用 */
    for (i = 0; i < prog->aux->used_btf_cnt; i++)
        btf_put(prog->aux->used_btfs[i].module);

    /* 程序内存(通过 kvfree 释放) */
    struct bpf_prog *orig = prog->aux->orig_prog;
    kvfree(prog->aux->func_info);
    kvfree(prog->aux->linfo);
    ...
    kvfree(prog->aux);
    kvfree_rcu(orig ?: prog, aux->rcu);
}

释放的顺序: 1. JIT 本机码的内存(bpf_jit_free_exec) 2. 引用的 map(bpf_map_put ——如果这是最后一个引用,map 自身也会被释放) 3. 引用的 BTF 模块 4. bpf_prog_aux 结构体(kvfree) 5. bpf_prog 结构体(kvfree_rcu ——使用 RCU 延迟释放,确保所有当前正在执行该程序的 CPU 都释放了对它的引用)

注意最后一步使用 kvfree_rcu,而非直接 kvfree。这是因为即使程序已经 detach 且引用计数归零,仍有极小的概率有一个 CPU 正在执行该程序的最后几条指令。RCU grace period 保证这个窗口被安全覆盖。

七、FD 泄露:最常见的生产故障模式

7.1 泄露场景

场景 1:尾调用程序 map 泄露

/* BPF 程序 A 尾调用程序 B、C、D */
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 4);
    __uint(key_size, sizeof(u32));
    __uint(value_size, sizeof(u32));
} jmp_table SEC(".maps");

/* 用户态将 B、C、D 的 FD 放入 jmp_table */
bpf_map_update_elem(map_fd, &idx0, &prog_b_fd, BPF_ANY);
bpf_map_update_elem(map_fd, &idx1, &prog_c_fd, BPF_ANY);
bpf_map_update_elem(map_fd, &idx2, &prog_d_fd, BPF_ANY);

BPF_MAP_TYPE_PROG_ARRAY 的每个条目持有目标程序的引用(aux->refcnt)。如果主程序被卸载但 jmp_table map 没有被显式清理,B、C、D 的引用计数永远不会归零。

场景 2:pin 后遗忘 unpin

# 部署时
bpftool prog load my_prog.o /sys/fs/bpf/prog_xyz

# 更新时:load 新版本,但旧版本仍然 pin 在 bpffs 上
bpftool prog load my_prog_v2.o /sys/fs/bpf/prog_xyz

# 旧程序仍然存在(如果 pin 路径不同的话)

场景 3:map-in-map 循环引用

/* 外层 map 存储内层 map 的 FD */
struct {
    __uint(type, BPF_MAP_TYPE_HASH_OF_MAPS);
    ...
} outer SEC(".maps");

如果用户态进程将内层 map 放入 outer map 后退出,内层 map 的引用由 outer map 持有。如果 outer map 没有被释放,内层 map 也永远不会释放。

7.2 检测与排查

# 1. 列出所有已加载的 BPF 程序
bpftool prog show
# 输出示例(经删减):
# 123: xdp  name count_packets  tag abcd...
#       loaded_at 2026-06-12T10:00:00+0000  uid 0
#       xlated 24B  jited 39B  memlock 4096B

# 2. 检查特定程序的引用
bpftool prog show id 123

# 3. 列出所有 map
bpftool map show

# 4. 列出所有 link
bpftool link show

# 5. 查看 bpffs 中 pin 的对象
ls -la /sys/fs/bpf/
bpftool prog show pinned /sys/fs/bpf/

# 6. 检查 FD 使用(通过 /proc)
ls -la /proc/<pid>/fd/ | grep bpf-prog
cat /proc/<pid>/fdinfo/<fd>
# 输出包含 prog_id、prog_type、prog_tag、prog_jited 等

# 7. 统计当前所有 BPF 程序数量
bpftool prog show | grep -c '^[0-9]'

典型泄露症状:bpftool prog show 输出的程序数量持续增长,但没有任何用户态进程在运行。这些程序就是”僵尸 BPF 程序”——它们的 aux->refcnt 因为某些原因(pin、subprog 引用、map-in-map)没有归零。

7.3 预防原则

  1. 使用 bpf_link 而非裸 attachbpf_link 提供 RAII——FD 关闭自动 detach。Pin link 而不是 pin prog。
  2. 部署脚本显式清理:在加载新程序前,先 bpftool prog show 确认没有旧版本残留。
  3. map-in-map 注意所有权:明确哪个程序/进程是内层 map 的所有者,谁负责最后的释放。
  4. 尾调用数组注意清理:在卸载主程序前,先把 PROG_ARRAY 中的条目置零。
  5. 使用 bpffs 目录组织/sys/fs/bpf/<app>/progs//sys/fs/bpf/<app>/maps/——便于批量清理。

八、总结

BPF 程序生命周期的核心是一个命题:程序在内核中的存在,完全由引用计数决定

单一的 aux->refcnt 将不同来源的引用(用户态 FD、attach、pin、prog_array 等)汇聚到同一计数器——close(fd) 只释放 FD 那份引用,detach 只释放 attach 那份引用,两者互不替代。只有 aux->refcnt 归零才释放内存。

各种 attach 类型在生命周期上的差异,本质上是”谁持有引用”的差异: - bpf_link:link 对象持有引用,FD 关闭就释放(除非 pin) - cgroup:cgroup 结构体持有引用,cgroup 删除才释放 - XDP/TC:net_device 持有引用,设备删除才释放 - raw tracepoint:perf_event 持有引用,FD 关闭就释放

理解这些差异,才能在生产中正确管理 BPF 程序的生命周期——避免僵尸程序累积、避免意外 detach、避免 pin 后遗忘 unpin。

下一篇(第 10 篇)将从 libbpf 的加载器工程角度,拆解 skeleton、auto-attach、map pinning 和 ringbuf consumer 的工程实践。

参考

内核源码

规范与文档

补充资料

上一篇Helper 函数子系统:注册、类型检查与参数传递(第 08 篇)

下一篇libbpf 加载器工程:skeleton、auto-attach、map pinning(第 10 篇)

同主题继续阅读

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


By .