一个 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.c、kernel/bpf/core.c、kernel/bpf/inode.c、include/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 file,filp->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,出错时直接释放。
步骤 4:bpf_check() 执行
verifier。这是整个加载流程中最耗时也最复杂的部分(参见第
02、03 篇)。如果 verifier
拒绝,程序不会产生任何副作用——没有分配
ID,没有获得引用,直接 goto free_prog。
步骤 5:bpf_prog_alloc_id()
分配唯一的
prog->aux->id。此时程序获得内核身份,可以被
bpftool 看到。
步骤
6:bpf_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;
}步骤 7:bpf_prog_new_fd()
创建一个 anon_inode 类型的
struct file,filp->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_event 的 release 路径调用
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()
释放所有存储的程序。
4.4 XDP/TC:netlink attach
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_prog5.4 Bpf_link pin:持久化 attach
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 预防原则
- 使用
bpf_link而非裸 attach:bpf_link提供 RAII——FD 关闭自动 detach。Pin link 而不是 pin prog。 - 部署脚本显式清理:在加载新程序前,先
bpftool prog show确认没有旧版本残留。 - map-in-map 注意所有权:明确哪个程序/进程是内层 map 的所有者,谁负责最后的释放。
- 尾调用数组注意清理:在卸载主程序前,先把
PROG_ARRAY中的条目置零。 - 使用 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 的工程实践。
参考
内核源码
- Linux 6.8
kernel/bpf/syscall.c—bpf_prog_load()、bpf_prog_new_fd()、bpf_prog_release() - Linux 6.8
kernel/bpf/core.c—bpf_prog_select_runtime()、bpf_prog_free() - Linux 6.8
kernel/bpf/inode.c—bpf_obj_pin()、bpf_obj_get()、bpffs 文件系统操作 - Linux 6.8
include/linux/bpf.h—struct bpf_prog、struct bpf_prog_aux、struct bpf_link - Linux 6.8
kernel/bpf/cgroup.c— cgroup BPF attach/detach - Linux 6.8
net/core/dev.c— XDP attach(dev_xdp_attach) - Linux 6.8
kernel/bpf/bpf_struct_ops.c— struct_ops 生命周期
规范与文档
Documentation/bpf/bpf_design_QA.rst— BPF 设计 Q&A(含生命周期相关讨论)Documentation/bpf/syscall_api.rst— BPF syscall API 文档
补充资料
上一篇:Helper 函数子系统:注册、类型检查与参数传递(第 08 篇)
下一篇:libbpf 加载器工程:skeleton、auto-attach、map pinning(第 10 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】JIT 编译器后端:x86-64 与 ARM64 的 BPF→Native 翻译管线
从 bpf_jit_compile() 入口出发,拆解 BPF 字节码到 x86-64/ARM64 本地指令的翻译过程——寄存器映射策略、ALU 指令的 one-to-one/many-to-one 翻译、尾调用与 call 的本地实现、JIT 镜像的 kallsyms 集成,以及 JIT 与 interpreter 的性能边界。
【eBPF 内核实现深度拆解】Map 内核实现(上):hash / array / per-CPU 的数据结构与并发模型
从 bpf_map_ops 虚函数表出发,逐层拆解 BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_ARRAY、per-CPU 变体的内核实现——htab 的 bucket 链表与 prealloc、bpf_array 的零拷贝共享、per-CPU 分配器的无锁语义。