为什么 libbpf 在对象初始加载时会跳过手动策略程序,struct ops 的 map 初始化遍又为什么会把经接口设置的手动加载策略覆盖回去?
因为批量加载遍只加载加载策略为 AUTO 的程序,并故意把 MANUAL 程序保持未加载,好让这个程序之后能被单独加载或卸载。覆盖来自更早的一遍,它跑在对象 prepare 之内:内核 BTF 的 struct ops map 初始化遍在一个回调槽位无法解析时,会无条件地把 DISABLED 写回程序的加载策略,完全不看用户设置的是什么。于是用户经接口设置的 MANUAL 被悄悄改写成 DISABLED,之后的显式加载因为策略已不再是 MANUAL 而以 -EINVAL 失败。紧接其后运行的 autoload 调整遍无法恢复这个意图,因为它只在程序仍读作 MANUAL 时才保护该程序。
机制
-
主线现状。 程序结构体只带一个
bool autoload标志,已发布的头文件只暴露 autoload 访问器,对象加载是单一遍。带SEC("?...")前缀的段名会把 autoload 关掉,该程序在加载时被跳过,其余程序都保持打开。已发布的符号表把卸载入口列在它最初的LIBBPF_0.0.1段里,而且主线那个卸载还会一并释放程序的 func_info、line_info 与 subprogs 数组;下面描述的「仅关闭描述符」的更窄行为属于在途系列,不是当前主线。 -
在途 v9 系列。 该系列把 autoload 布尔量换成三态加载策略:
DISABLED(跳过,等价于SEC("?..."))、AUTO(在对象加载时加载,默认值)、MANUAL(被批量遍跳过,之后经新增的单程序加载入口加载)。声明式的SEC("!...")前缀映射到MANUAL,与?约定镜像。setter 只在对象 prepare 之前生效,并且对子程序设置手动策略会被拒绝。单程序的显式加载按顺序检查前置条件:对象尚未 prepare、该程序是子程序、策略不是MANUAL,这三种情况各自返回-EINVAL;程序已经加载(仍持有文件描述符)则返回-EBUSY。还有两个推论。带任何手动程序的对象会拒绝生成 loader,因为生成 loader 在对象加载时定稿,而对象加载会跳过手动程序。另外,模块 BTF、vmlinux BTF 与文件描述符数组会在有手动程序存在期间保留,直到对象关闭才释放;手动程序保留其重定位描述符,以便稍后加载时 CO-RE 校验器日志修补仍然生效。 -
struct ops 的断裂。 struct ops 程序无法独立加载或卸载,因为内核绑定的是整个 struct ops map,map 槽位持有程序的文件描述符。该系列因此在解析段名时以
-EINVAL拒绝 struct ops 的声明式手动前缀,但接口 setter 仍然接受给这些程序设手动策略 —— 开放问题恰好落在这条路径上。在对象 prepare 期间,struct ops map 初始化遍紧跑在 autoload 调整遍之前。遍内若某个回调成员在运行内核的 BTF 中不存在,或某个 struct ops map 影子结构里的槽位被清空、或被另一个程序替换,初始化遍就会无条件把该程序的加载策略赋值为DISABLED。
验证与排障路径
- 读回策略。 设置后立即用 getter 读回,确认它仍为
MANUAL。对象 prepare 完成后再读一次:若某个 struct ops 程序读回DISABLED,说明覆盖已发生在 map 初始化遍之内,来自缺失的回调槽位,或被清空、被替换的 map 槽位。 - 区分两种结果。 autoload 调整遍随后运行,此时它已看不到需要保护的手动策略,于是出现两种情况之一。若没有别的东西引用该程序,它就停在
DISABLED,显式加载返回-EINVAL。若另一个自动创建的 struct ops map 仍引用该程序,调整遍会把它设为AUTO,程序在普通对象加载期间被加载,而且完全不报错:手动加载根本没有发生。第二种静默结果更难发现,因为程序确实挂上了,而调用方原本要求由自己控制它。 - 检查延迟清理边界。 只要存在手动程序,模块 BTF、vmlinux BTF 与文件描述符数组就是在对象关闭时释放,而不是在对象加载返回时释放。不要假设这些对象在加载返回后就不在了;也不要假设对象关闭后,手动程序的稍后加载还能去查询它们。
局限
- 加载策略枚举、单程序加载入口与手动
SEC("!...")前缀都不在主线上。已发布的头文件与符号表只带 autoload 标志访问器与卸载入口,符号表里最新的已发布符号版本仍是一个空段。 - struct ops 程序实际上无法延迟:内核绑定整个 struct ops map,单个这样的程序无法单独加载或卸载。
- 覆盖是当前修订版上的开放评审发现,严重级别为中等:建议加的守卫应落在 map 初始化遍,也就是写入
DISABLED的那一遍。 - 生成 loader 与任何手动程序都不兼容,会以
-EOPNOTSUPP失败;而且在 prepare 步骤之后再设置该 loader 同样会被拒绝,所以手动策略与生成 loader 无法组合使用。
References
- Kernel
tools/lib/bpf/libbpf.h— 已发布的 autoload 访问器与SEC("?...")约定。 - Kernel
tools/lib/bpf/libbpf.map— 卸载入口位于最初的LIBBPF_0.0.1段,且没有单程序加载符号。 - 公开 bpf 邮件列表 cover letter:libbpf BPF program manual loading, v9 — 三态策略、动机,以及手动程序与生成 loader 的拒绝。
- 公开 bpf 邮件列表 patch 4/7:BPF programs manual loading and attaching — 单程序加载的前置条件及其错误码、struct ops 与生成 loader 的拒绝、延迟的对象清理。
- patch 4/7 的自动化评审 — struct ops map 初始化会在 autoload 调整遍之前覆盖用户设置的手动策略这一发现。
当日社区讨论
本次运行的观察清单覆盖六个社区:两个聊天工作区共十个频道,两个由存档支撑的聊天工作区共三个频道,一个邮件列表,一个实践者论坛。其中两个在本窗口内带有真实技术讨论:公开 bpf 邮件列表(经其公开存档页面阅读)与实践者论坛(经其普通公开列表页阅读)。那三个存档支撑的频道经已批准只读存档按 24 小时与 7 天窗口查询,均未返回任何消息,因此它们是安静而非跳过。两个聊天工作区的十个频道本次未覆盖,因为读取它们需要一次已登录的浏览器会话而本次没有;它们被记为未覆盖,而非安静。
手动加载系列恰好发布于本次窗口之内,并给出了手动策略完整的量化动机:一个大型工具在任意系统上约有 250 个可加载程序,但默认只加载约 90 个;把默认加载集缩小后,作者自测的加载时间下降约六成,而卸载耗时随已加载程序数量线性变化。它身上的评审流量目前全由机器产生:自动化评审回复了两次,其中一次就在手动加载补丁上给出上面的策略覆盖发现,而截至本次运行还没有人工回复。cover letter 变更说明里引用的评审意见属于更早的修订版,所以本题描述的那条边界是真的仍然开放,而不是只是尚未定论。
本窗列表的其余部分由校验器与加载器工作主导,而不是新的挂载 API。一个重做已校验指令补丁应用方式的大型系列——删除空操作指令、对常量做盲化、重映射校验器状态,以及在补丁把反向有界循环跳转推出范围时把它放宽——在本窗口引发了很长的评审讨论,其中循环跳转是最需要留意的一条:一个把代码改长的补丁可能让校验器先前已接受的跳转距离失效。另有一个系列提出一个针对内核符号表的查询接口,以及一条经它解析无类型内核符号的加载器路径;评审集中在接口形态而非思路本身。关于在内核函数里读取与映射用户内存的系列,以及两个 socket map 清理——标注拆除期的数据竞争,以及用一个测试覆盖一次并发的增删——也在本窗口内接受评审;此外还有零拷贝收包路径上关于发送侧校验和与缓冲区释放的修复。合起来看,当前的力气花在校验器的补丁处理与更安全地访问 eBPF 程序此前难以干净触达的内存上,而不是花在新的挂载机制上。
实践者论坛在本窗 24 小时之内的唯一条目,是一篇关于大规模运行 eBPF 的生产经验帖;那里最新的技术线程已有几天,延续了一条跨多天的报告:一个基于零拷贝包 socket API 拷贝模式构建的报文构造扫描器。它的墙钟时间问题出在单个发送环上每包一次发送系统调用,把每次调用批量到最多六十四个帧之后,与常规扫描器的差距大部分被追回;而绑定到某个核心的忙轮询循环在负载很重的虚拟机上反而有害,因为自旋线程在与其他负载争抢 vCPU。那里还有两个未解的问题,也正是工程师会以同样方式撞上的:在生产者本会改为轮询的情形下,内核唤醒标志能带来多少收益;以及只有一个发送环时,除了批处理还有什么能提升吞吐。暴露该唤醒机制的绑定标志与其背后的环形队列布局规定在 内核 AF_XDP 文档 里;唤醒是否有益取决于负载,该线程没有可用于定论的测量。另有一个近期的帖子询问,作者经可观测性探针组件接触到 eBPF 之后,究竟哪些安全工具真的在用 eBPF —— 这是一个值得记下的发现路径信号:新实践者从可观测性产品进入,然后去找安全场景。
本次运行的存档来源在两个窗口内均未产出消息,因此上面的问题取自被允许的公开 bpf 邮件列表,并经其补丁线程与已发布内核源码核验;实践者论坛提供了本节中的运维主题。
继续阅读
返回索引
eBPF 问答
这里每天收录一个关于 eBPF、Linux 可观测性、性能分析、运行时扩展或安全问题的回答。每篇回答都以公开的一手资料为依据,并在末尾附上匿名化的当日社区讨论摘要。
上一篇 / 上一页
为什么 iocost 的 cgroup 代价模型文件只能在内建模型的 auto 与 user 系数集之间切换,而 BPF struct ops 代价模型接管设备之后,这个文件为什么不再选择模型?
因为 cgroup 文件是内建模型的接口,而 BPF 程序是带外挂载的。blk-iocost 的 cgroup 代价模型文件的写路径解析 ctrl= 令牌(auto 或 user)、model= 令牌(恒为 linear)和七个线性系数令牌,并翻转 usercostmodel 位;它没有任何可运行的程序。公开的补丁系列把一个 BPF struct ops 实现绑定到某个设备的 iocost 实例上,经由 struct ops link
下一篇 / 下一页
为什么生成式 BPF loader 对象会在只有子程序或未加载程序把数量推过上限时被拒绝,而检查又如何移到真实的已加载程序数量上?
因为上限是针对对象的程序总数强制执行的,而这个总数把 loader 根本不会去加载的子程序与关闭了 autoload 的程序也算了进去,于是本该能加载的对象也被拒了。修法是把代码生成阶段的记账夹到上限之内,并把真正的拒绝推迟到「程序文件描述符真正写入 loader 栈」的那个时刻;只有那些真正通过了加载遍的程序才会走到那里。
- 最后更新
- 2026年10月30日
- 首次发布
- 2026年10月30日
- 贡献者
- Yunwei
这个页面有帮助吗?