LWN:Linux 5.3会支持BPF里的有限循环代码

640点击上方蓝色“ Linux News搬运工”关注我们~


Bounded loops in BPF for the 5.3 kernel

July 31, 2019

This article was contributed by Marta Rybczyńska


BPF program在过去几年已经增加了很多功能,目前能进行不少很实用的操作了。此前,BPF开发者一直得想办法绕过一个很讨厌的限制:无法使用循环。近期Alexei Starovoitov提交的一组patch set合入Linux 5.3之后,终于能够解决这个问题了。除了增加循环功能支持以外,也顺便大大降低了大多数BPF program的加载耗时。


The problem


在BPF program运行之前,需要先经过检查确保它不会损害系统。例如,如果这个BPF program不能在有限时间内结束的话,就可能被利用来做denail-of-service(DoS拒绝服务)攻击。BPF verifier就是用来检查BPF program的。此前BPF verifier一直不能支持循环,也就是说所有带有循环的program都会被拒绝加载。


毕竟循环是计算机程序的一个最基本的能力,所以开发者经常会碰到需要使用循环的情况。此时他们只好绕过这个问题,例如把循环拆解成很多条重复指令(要么自己手动修改,要么利用编译器的pragma)。因此不难理解大家都在想方设法希望能改善现状,能拥有一个有限循环的支持。曾经Edward Cree就提供了一个概念型的方案,还有John Fastabend也曾经想利用循环分析来解决这个问题:确定出每个循环的induction variable(归纳变量,跟循环执行的次数有关联)以及它是怎么用的。Fastabend在2018 Linux Plumbers Conference上介绍了这个理论背景以及解决方案。kernel开发者对用这两种方案都表示了反对。


与此同时,在BPF verifier的优化方面有了更多投入,在Linux Storage, Filesystem and Memory Management Summit的Jakub Kicinski进行了介绍。这里一个主要改动就是在5.2 kernel里增加了BPF program的大小限制,一个program能最多执行的4096个指令改为了1百万条指令。这次的改动对于原有的循环限制就引出了一个更加直接、甚至有点暴力的解决方案:不用对循环增加特殊处理,可以让BPF verifier直接把循环的过程模拟成大量状态集合,跟它原本支持的其他状态集合没有什么区别。


The BPF verifier


当BPF verifier在分析一个program的时候,会以状态机的形式来建立模型。它会检查每个状态,是否包含不合适的行为。检查的过程中会记录下来它所验证过的那些状态。后来,当它走到一个此前已经验证过的状态的时候,它就认为这条路径上就验证完毕了(pruning point),不必继续深入了。这样就能大大减少verifier所要做的工作。


这种基于状态的简化过程对verifier的性能非常重要,不过这也增加了它的消耗,包括比较各个状态,以及把安全状态复制备份。近期对网络相关的BPF program进行了一些分析,发现保存下来的状态里面80%的都不会再匹配到,因此不会对后续的搜索简化提供帮助。因此,如果能减少verifier所维护的状态数量以及逻辑判断点,那么就能提升系统的性能。


此前优化的做法是在每个跳转指令之前之后都放一个逻辑判断点(pruning point),这样平均每4条指令就有一个逻辑判断点。后来发现如果每10条指令放一个逻辑判断点,甚至不用管具体是什么指令,都能大大改善性能。BPF verifier还有个改动是更激进地丢弃那些保存下来但是没有对路径精简有贡献的状态。这个改动又提升了20%的性能,使得相同时间内能分析的program代码长度提升了1/3。


Adding bounded loops


Starovoitov的patch set在前面所说的改动基础上,还引入了bounded loops的概念。也包含了不少其他改进。第一个patch引入的一些性能损失其实是最终解决方案的基础。具体说到这个性能损失,它是因为在堆栈上引入variable tracking(变量跟踪)所导致的。编译器在想要释放一些寄存器的时候,经常会把变量放到堆栈上(称之为"spills" them)。BPF verifier需要能跟踪住这类变量,因为有可能会需要根据这些变量的内容来做决定。此前一直没有实现这个功能,导致某些特定的program会被错误的判定为不合格。目前看来循环指令相关的变量就经常被放到堆栈上,那么实现相应的变量跟踪就是要想实现循环终结点判断的前提条件了。


此外,跟踪变量的某些特定的值(不是指跟踪可选值的一个范围)也会创造更多状态,从而让状态精简的效率变低。如果在堆栈上跟踪,那情况就更加坏了。状态数量不断增加,所以验证时间也会变长。在调查这个问题的时候,Starovoitov发现了一个现象,性能损失会受到Clang编译器里改动影响而放大,最后查出来新版的Clang编译器会对堆栈上放置更少的变量,在没有完整的变量跟踪的情况下就减少了状态精简。这两个问题合在一起导致verifier的性能下降,正如作者在commit message里所描述的。


有限循环支持,需要另一个功能,就是扩展BPF verifier对条件分支的支持方式。如果是对两个常量进行比较,verifier可以很轻松的判断出来是否需要进行分支跳转。如果比较的对象里面牵扯到了变量,那确实是更加困难的。这也就是为什么直到现在,verifier都只支持比较一个寄存器和一个常亮。在此patch set中,Starovoitov支持了两个寄存器之间的比较判断。这也是为了能模拟循环中的条件判断。


第三点,也是最后一点,就是对所有这些状态之间增加了父子关系。当verifier需要探索program的两个分支的时候,它会把这两者都当做父状态的两个子状态。并且也会统计所需要探索的分支数量,这样就能知道还剩多少分支需要处理。拥有这些功能之后,再加上对状态精简的过程中采用更加激进的启发式条件,BPF verifier可以支持有限循环了。它仅仅需要把执行中所有可能的循环次数(all possible iteration)都模拟出来就好。


拥有有限循环支持之后,只剩下一项工作:在第一个patch里面引入的性能下降。相应的方案在最后一个patch里实现了,是基于此前实现的那些改善点的(尤其是利用了状态间的父子关系),Starovoitov增加了precise scalar value的跟踪。这些值都是存在寄存器里的,会在程序执行时被修改。BPF verifier需要拿到精确值来分析是否要进行分支跳转,不过它不用精确跟踪每个寄存器的值,只对那些控制分支跳转的变量需要拿到准确值。这样下来verifier就不会为了跟踪所有寄存器的准确值而付出大代价了,相反,当需要拿到某个精确值的时候,它会回溯(backtrack)一下寄存器的使用,尽量能拿到精确值。


Summary


BPF verifier经过这组patch set的系列改动之后,不仅能支持有限循环了,并且还拥有了很多重要的优化。在5.3 kernel之后,为kernel写BPF program会更加容易了,不过,相信还是有不少BPF developer会继续抱怨这里其他遗留的那些坑,需要绕过这些,才能让BPF verifier判断他们的program是安全的。


全文完

LWN文章遵循CC BY-SA 4.0许可协议。

极度欢迎将文章分享到朋友圈 
热烈欢迎转载以及基于现有协议修改再创作~


长按下面二维码关注:Linux News搬运工,希望每周的深度文章以及开源社区的各种新近言论,能够让大家满意~


640?wx_fmt=jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值