跳转到主要内容

为什么 libbpf 在对象初始加载时会跳过手动策略程序,struct ops 的 map 初始化遍又为什么会把经接口设置的手动加载策略覆盖回去?

因为批量加载遍只加载加载策略为 AUTO 的程序,并故意把 MANUAL 程序保持未加载,好让这个程序之后能被单独加载或卸载。覆盖来自更早的一遍,它跑在对象 prepare 之内:内核 BTF 的 struct ops map 初始化遍在一个回调槽位无法解析时,会无条件地把 DISABLED 写回程序的加载策略,完全不看用户设置的是什么。于是用户经接口设置的 MANUAL 被悄悄改写成 DISABLED,之后的显式加载因为策略已不再是 MANUAL 而以 -EINVAL 失败。紧接其后运行的 autoload 调整遍无法恢复这个意图,因为它只在程序仍读作 MANUAL 时才保护该程序。

机制

  1. 主线现状。 程序结构体只带一个 bool autoload 标志,已发布的头文件只暴露 autoload 访问器,对象加载是单一遍。带 SEC("?...") 前缀的段名会把 autoload 关掉,该程序在加载时被跳过,其余程序都保持打开。已发布的符号表把卸载入口列在它最初的 LIBBPF_0.0.1 段里,而且主线那个卸载还会一并释放程序的 func_info、line_info 与 subprogs 数组;下面描述的「仅关闭描述符」的更窄行为属于在途系列,不是当前主线。

  2. 在途 v9 系列。 该系列把 autoload 布尔量换成三态加载策略:DISABLED(跳过,等价于 SEC("?..."))、AUTO(在对象加载时加载,默认值)、MANUAL(被批量遍跳过,之后经新增的单程序加载入口加载)。声明式的 SEC("!...") 前缀映射到 MANUAL,与 ? 约定镜像。setter 只在对象 prepare 之前生效,并且对子程序设置手动策略会被拒绝。单程序的显式加载按顺序检查前置条件:对象尚未 prepare、该程序是子程序、策略不是 MANUAL,这三种情况各自返回 -EINVAL;程序已经加载(仍持有文件描述符)则返回 -EBUSY。还有两个推论。带任何手动程序的对象会拒绝生成 loader,因为生成 loader 在对象加载时定稿,而对象加载会跳过手动程序。另外,模块 BTF、vmlinux BTF 与文件描述符数组会在有手动程序存在期间保留,直到对象关闭才释放;手动程序保留其重定位描述符,以便稍后加载时 CO-RE 校验器日志修补仍然生效。

  3. struct ops 的断裂。 struct ops 程序无法独立加载或卸载,因为内核绑定的是整个 struct ops map,map 槽位持有程序的文件描述符。该系列因此在解析段名时以 -EINVAL 拒绝 struct ops 的声明式手动前缀,但接口 setter 仍然接受给这些程序设手动策略 —— 开放问题恰好落在这条路径上。在对象 prepare 期间,struct ops map 初始化遍紧跑在 autoload 调整遍之前。遍内若某个回调成员在运行内核的 BTF 中不存在,或某个 struct ops map 影子结构里的槽位被清空、或被另一个程序替换,初始化遍就会无条件把该程序的加载策略赋值为 DISABLED。

验证与排障路径

  1. 读回策略。 设置后立即用 getter 读回,确认它仍为 MANUAL。对象 prepare 完成后再读一次:若某个 struct ops 程序读回 DISABLED,说明覆盖已发生在 map 初始化遍之内,来自缺失的回调槽位,或被清空、被替换的 map 槽位。
  2. 区分两种结果。 autoload 调整遍随后运行,此时它已看不到需要保护的手动策略,于是出现两种情况之一。若没有别的东西引用该程序,它就停在 DISABLED,显式加载返回 -EINVAL。若另一个自动创建的 struct ops map 仍引用该程序,调整遍会把它设为 AUTO,程序在普通对象加载期间被加载,而且完全不报错:手动加载根本没有发生。第二种静默结果更难发现,因为程序确实挂上了,而调用方原本要求由自己控制它。
  3. 检查延迟清理边界。 只要存在手动程序,模块 BTF、vmlinux BTF 与文件描述符数组就是在对象关闭时释放,而不是在对象加载返回时释放。不要假设这些对象在加载返回后就不在了;也不要假设对象关闭后,手动程序的稍后加载还能去查询它们。

局限

  • 加载策略枚举、单程序加载入口与手动 SEC("!...") 前缀都不在主线上。已发布的头文件与符号表只带 autoload 标志访问器与卸载入口,符号表里最新的已发布符号版本仍是一个空段。
  • struct ops 程序实际上无法延迟:内核绑定整个 struct ops map,单个这样的程序无法单独加载或卸载。
  • 覆盖是当前修订版上的开放评审发现,严重级别为中等:建议加的守卫应落在 map 初始化遍,也就是写入 DISABLED 的那一遍。
  • 生成 loader 与任何手动程序都不兼容,会以 -EOPNOTSUPP 失败;而且在 prepare 步骤之后再设置该 loader 同样会被拒绝,所以手动策略与生成 loader 无法组合使用。

References

当日社区讨论

本次运行的观察清单覆盖六个社区:两个聊天工作区共十个频道,两个由存档支撑的聊天工作区共三个频道,一个邮件列表,一个实践者论坛。其中两个在本窗口内带有真实技术讨论:公开 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

这个页面有帮助吗?