关注了就能看到更多这么棒的文章哦~
A possible path for cancelable BPF programs
By Daroc Alden
February 25, 2025
Gemini-1.5-flash translation
https://lwn.net/Articles/1010404/
Linux 内核支持将 BPF 程序附加(attach)到许多操作(operations)上。这通常是安全的,因为 BPF 验证器(BPF verifier)确保 BPF 程序不会滥用内核资源、无限期运行或以其他方式来越过界限。然而,在尝试扩展 BPF 程序的功能和确保验证器能够处理每个边缘情况之间,一直存在着一些冲突。2 月 14 日,Juntong Deng 分享了 一个概念验证(proof-of-concept)补丁集,该补丁集向 BPF 添加了一些运行时检查,以便将来可以中断正在运行的 BPF 程序。
最初构思时,BPF 对程序可以包含的指令数量有严格的限制,并且不允许循环,从而限制了程序可以运行的时间。这一点很重要,因为内核会在许多时间敏感的操作期间调用 BPF 钩子,因此,行为不端的 BPF 程序如果设法运行太长时间,可能会导致内核挂起或其他问题。随着时间的推移,这些限制逐渐扩大,原因有二:验证器变得更加强大,可以处理循环、更复杂的函数等;开发人员发现复杂、长时间运行的 BPF 程序非常有用,促使他们要求放宽限制。
理论上,BPF 可以使用某种看门狗(watchdog)系统来杀死运行时间过长的程序,而不是试图静态地限制程序的运行时间。这将具有一些优势,例如允许具有验证器当前无法理解的复杂控制流的程序。这个想法的主要问题是 BPF 程序可以持有内核资源,例如锁,因此杀死它们不像中断它们的执行那么简单。
Deng 的补丁集通过添加相对低开销的已获取内核资源跟踪来解决这个问题。通过动态跟踪,杀死 BPF 程序的过程变得更加简单,因为内核可以随时释放程序持有的任何资源。该补丁集尚未实现实际的看门狗以及相关的 BPF 程序终止功能。相反,它更像是一个概念验证,旨在表明这种方法可以与 BPF 对内核资源的使用一起工作。
Deng 还明确表示,即使补丁集准备就绪,它仍然只是一个中间步骤。验证器应该继续改进,直到它可以静态地跟踪需要在程序的每个点清理哪些资源:
请注意,此补丁系列并非旨在取代预运行时/后运行时的工作,并且没有运行时开销始终比有运行时开销更好。我们的最终目标是完全没有运行时开销。将来,随着这些无运行时开销的解决方案成熟,可以禁用运行时开销的解决方案。
这类似于 BPF JIT 和 BPF 解释器之间的关系。我们始终知道 JIT 更好,并且最终应该使用,但是当 JIT 尚未准备好或无法使用时,解释器是一个不错的替代方案,可以帮助我们更快地支持某些功能。
详情
任何解决方案都需要表现良好,因为 BPF 程序在内核的性能敏感部分中非常普遍。在这种情况下,这意味着避免为资源跟踪进行动态分配。幸运的是,验证器已经跟踪 BPF 程序何时获取或释放资源。Deng 的补丁集添加了代码来检查程序同时持有的最大资源数,并分配一个静态表来保存资源信息。该表保存一系列槽(slot),这些槽被初始化为各自保存指向下一个槽的指针,从而形成空闲槽的链表。
该表不仅需要保存指向相关资源的指针,还需要存储资源的类型。BPF 程序可以持有几种不同类型的内核资源,所有这些资源都需要使用特定的函数来获取和释放。例如,=bpf_task_from_pid()= 用于获取对 task_struct
的引用,该引用通过 bpf_task_release()
释放。
为了基于资源类型查找正确的释放函数,Deng 的补丁构建了一个类型表。理论上,当 BPF 程序被终止时,代码可以在表中进行二分查找以找到资源类型,从而找到其释放函数。但是,大多数时候,BPF 程序会正常释放资源。发生这种情况时,内核需要在资源表中找到该资源的条目,并将其添加到空闲槽列表中。这是通过使用固定大小的哈希表来完成的,以便查找速度很快。
总体而言,Deng 的补丁为获取和释放资源的过程增加了一小部分运行时开销。在获取时,代码需要使用二分查找来查找资源类型,从空闲槽列表中弹出一个槽,并将一个条目插入到哈希表中。在释放时,代码需要在哈希表中查找该条目,将其从表中删除,并将一个槽推送到空闲槽列表中。所有这些步骤平均都需要恒定的时间,但查找资源类型除外,这需要的时间与可以通过这种方式跟踪的 BPF 类型数量成对数关系。
为了实际完成这项工作,该补丁修改了验证器,以便每次程序调用获取或释放函数时,都插入对钩子函数的调用,该钩子函数执行记账,然后将调用转发到实际函数。这对于 kfunc(暴露给 BPF 的内核函数)效果很好,这意味着 kfunc 本身不需要修改。Kfunc 已经使用 KF_ACQUIRE
和 KF_RELEASE
标志进行注释,以指示它们参与获取或释放资源。这些函数(在 6.13 内核中有 39 对)都具有相似的签名,仅采用其关联类型的单个参数。Deng 的代码使用该信息来确定 kfunc 和类型之间的关联。
但是,这种方法不适用于 BPF 辅助函数(helper,通过一种较旧且更脆弱的机制公开的函数)。辅助函数没有相同的注释。要完整地解决使 BPF 程序可终止的问题,将需要硬编码有关哪些 BPF 辅助函数与不同类型关联的信息。
Deng 后来建议使用相同的钩子机制为 BPF 程序添加运行时跟踪。验证器可以在特殊的调试模式下,为每个 kfunc 添加钩子,而不仅仅是与获取或释放资源相关的 kfunc。这可能会扩展诸如内核的 ftrace 机制之类的内容,以使其与 BPF 程序一起使用。
BPF 的未来
Deng 的补丁集尚未引起任何讨论,但是向 BPF 添加更多运行时检查的根本思想并不新鲜。BPF 依靠验证器来运行代码而无需大多数运行时检查,这是使其在语言运行时中与众不同的原因之一。其他编译语言可以使用复杂的分析来消除冗余检查或间接引用,但是它们通常通过从所有必要的检查开始,然后有选择地删除编译器可以证明不需要的检查来解决问题。另一方面,BPF 实质上使验证器无法理解的任何内容都成为程序员的问题。
BPF 的这种独特功能是否需要不惜一切代价地保留,从而要求内核开发人员避开任何引入运行时开销的方法?还是其他语言采用的更常见的方法是不可避免的未来,即试图推动 BPF 变得更强大?Deng 的补丁试图走一条中间道路,即引入具有运行时开销的功能,并期望当验证器得到改进以跟踪如何释放资源时,该功能将被替换。但是,很难不将对此类补丁的需求视为表明开发人员希望访问验证器无法提供的新功能。
验证器已经很复杂,并且只会变得更加复杂。随着时间的推移,这是否会驱使 BPF 开发人员不再在验证器中实现所有内容,还有待观察。无论哪种方式,该主题似乎都可能在未来几个月内引起一些讨论。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~