关注了就能看到更多这么棒的文章哦~
A different approach to BPF loops
By Jonathan Corbet
November 29, 2021
DeepL assisted translation
https://lwn.net/Articles/877062/
extended BPF 虚拟机的一个重要特点就是它有一个内置于内核之中的 verifier (验证器),它确保所有 BPF 程序的执行都是 safe 的。不过,BPF 开发者对 verifier 的看法是有点喜忧参半:虽然它能在很多问题发生之前抓住这些问题,但它也经常让人失望。因此有人将其比喻为一个善意、但是受到规则约束、并且很挑剔的官僚机构。Joanne Koong 提出的 bpf_loop()建议就是为了让某种类型的循环实现能让 BPF 中的这个官僚感到满意。
verifier 在检查时必须要模拟执行加载到内核里的每个 BPF 程序。它需要确保程序不会访问到一些不应该被它使用的内存、不会将内核内存泄漏给用户空间、以及其他许多检查条件,其中就包括能确保 BPF program 确实终止了,而不是将内核陷入一个无限循环之中。正如那些参加过算法课学习的开发者可以意识到的,要想证明一个程序会终止,这是一个困难的问题。事实上一般情况下这是无法证明的。因此,BPF verifier 不得不寻找一些方法来把这个问题简化下来。
最初的时候,"简化这个问题" 的方案就是完全禁止循环。当一个程序只能以按顺序继续执行(straight-through)的方式运行,不带有往回的 jump 时,很明显,程序肯定会在有限时间内终止。不用说,BPF 的开发者很快就发现这条规则有点太束手束脚了。一般来说,循环可以通过手动展开来模拟实现,但这对要写一个短小的循环的情况来说,会很烦人;对写长循环的场景来说,则非常不切实际。因此,人们很快就开始寻找是否有一种可以允许 BPF 程序包含循环的方法。多年来尝试了各种解决方法,最终在 2019 年的 5.3 内核中加入了有上限的循环的支持。
因此,这个问题得到了解决,至少是在一定程度上解决了。verifier 通过模拟初始状态的每个组合来检查循环,并证明每个循环会在执行所允许的最大数量的指令之前终止。这种验证可能需要一些时间,对于某些程序来说,verifier 根本无法得出循环最终会停止的结论,尽管这些程序可能是正确并且安全的。毕竟有太多的可能状态和迭代需要处理。
循环的验证还因为其他一些因素而变得更加复杂和困难,verifier 必须使用 BPF 代码来验证,这是一个非常底层的指令集。用高级语言写代码的时候定义循环的语义在这种底层语言处理过程中其实已经消失了。例如,代码可能只是对一个短小数组的元素进行遍历,但是 verifier 必须要从 BPF 指令序列中观察并得出这个结论。如果有一种方法可以让 verifier 看到有界循环的代码,那么处理起来就会容易得多。
简而言之,这就是 Koong 的 patch 的目的了。它增加了一个新的辅助函数,可以从 BPF 代码中调用:
long bpf_loop(u32 iterations, long (*loop_fn)(u32 index, void *ctx),
void *ctx, u64 flags);
每次调用 bpf_loop() 的时候就会变成不断调用 loop_fn(),其中的调用次数以及要传入的 ctx 参数就是这个函数的参数了。这里的 flags 参数目前还没有用起来,必须设为 0。loop_fn() 通常会返回 0,如果返回 1 的话就会让这个循环马上终止,不会有其他的返回值。
本质上讲,bpf_loop() 会将循环本身的机制从 BPF 代码中取出来,并将其嵌入内核的 BPF 实现中。这样就能让 verifier 立即知道循环最终会停止,因为这不在 BPF program 本身的控制之下。同时也很容易计算出在最坏的情况下会在循环中执行多少条指令了。避免无限循环,再配合上堆栈深度限制,就可以防止程序由于嵌套循环而几乎永远不断运行了。
对于 BPF 程序开发者来说,这样做的好处是任何可以用 bpf_loop()实现的循环都变得更容易通过 verifier 的检查了,那么复杂的官僚主义检查都被跳过了,就是这样。请注意,在遍历一个链表的循环也可以使用 bpf_loop() 实现,开发者只需要提供一个可能的上限次数作为迭代次数,然后只要找到了所需的元素、或者达到列表的末尾时就提前终止。BPF program 代码的写法会需要进行一些更改,变成遵照这种新的写法,但大多数情况下这个改动都是没有什么问题的。
另外有一个显著的优点,那就是验证 BPF 程序所需的时间大大减少,因为 verifier 不需要实际模拟执行所有这些循环了。一些基准测试也看到了这一点,一个在当前内核中需要近 30 秒时间来进行验证的程序可以在 0.15 秒内完成验证。这大大增加了许多种类的 BPF 程序的实用性。
Fortran 在数值应用中长期占据主导地位的原因有很多,其中一个原因就是 do 循环得益于其可预测的结构(predicable structure),相对容易被矢量化(vectorize)。bpf_loop() 的目的跟它不同,但它的工作机制是一样的:限制语言中可以表达的内容,使计算机更容易理解真正在做什么。反过来,这也可以使开发者更容易说服计算机他们的程序运行时是安全的。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~