关注了就能看到更多这么棒的文章哦~
A medley of performance-related BPF patches
By Jonathan Corbet
January 2, 2020
Kernel里放的BPF虚拟机带来众多好处,其中之一就是速度快。BPF program都是采用JIT(just-in-time)编译然后直接在CPU上运行,这样就不会有解释器来拖慢执行速度。不过在很多场景下,这还不够快。因此不出意料,近来有很多patch set在开发过程中,都是用来在不同方面继续加速BPF在系统里的执行速度的。其中有一些已经差不多要接近合入到mainline了。
The BPF dispatcher
BPF program需要在挂载(attach)到一个特定调用点之后才可以得到执行。例如tracing program会挂载到tracepoint位置,而网络XDP (express data path) program则会挂载到某个特定的网络设备。通常来说,在任何一个挂载点都可以挂载多个BPF program。这样在需要运行这个挂载点的BPF program的时候,kernel会依次调用一个linked list里面串起来的多个BPF program。
其实,在执行一个编译过的BPF program的时候,是通过一个间接跳转来实现的。这样的跳转其实本来就不算快,后来在speculative-execution漏洞泛滥的时候,大家把这个改成了retpolines,这种结构可以组织多种Spectre攻击,不过这也进一步拖慢了间接跳转的执行速度。对于那些需要频繁调用BPF program的场景,例如需要对每个网络包进行处理,那么这里拖慢的一点速度就会累积起来产生较大影响。
有不少人在做patch希望来降低repoline方式对性能的影响。Björn Töpel采用的方式是做了一组BPF dispatcher patch set,专门针对XDP应用场景的。这组patch维护一个机器编码的trampoline,其中包含针对每一个挂载了的BPF program的一条直接跳转指令。在BPF program加入或者离开list的时候,都必须重新生成这个trampoline。在需要调用某个BPF program的时候,会先调用这个trampoline并传入该BPF program的地址,接下来会进行二分法查找来获取对应的直接跳转指令,接下来会执行这个跳转,从而开始执行这个BPF program。
看起来这样做似乎为了替换一个间接跳转而引入了很多额外的开销。其实,这样仍然比利用retpoline的方式要快,根据patch set里提供的数据,大概快1/3。实际上间接跳转的开销非常大,哪怕没有retpoline,只用这个dispatcher来分发,效率也不会差。所以无论用不用retpoline,这个功能都会打开。这组patch已经在第五版了,看起来不久就有可能合入mainline了。
Memory-mappable maps
BPF map是供BPF program存放数据的。它有很多变种,不过基本上来说都是一些关联数组(associative array),可以跟其他BPF program或者user space程序共享使用的。在BPF program里面访问BPF map是通过一些专用的helper function来实现的。因为所有工作都在kernel里面完成,所以这里的访问速度很快。不过要是从user space获取一个BPF map的话,就得通过调用bpf()系统调用里的BPF_MAP_LOOKUP_ELEM和BPF_MAP_UPDATE_ELEM等操作。
如果用户只是想在tracing跑完之后读出结果,那么调用一次bpf()不算什么问题。不过如果user-space程序需要长时间运行并且会访问BPF map里面的大量数据的话,这个系统调用积累出来的开销可就不能忽视了。通常来说,要想得到比较好的性能数据,都需要尽量避免系统调用。所以假如每次要跟BPF program查一个数据都要调用一次系统调用的话,就是一个反例了。Andrii Nakryiko采用memory-mappable BPF maps部分的解决了这个问题。他允许user-space进程来把BPF array map(利用整形数来作为索引)直接映射到进程的地址空间里,这样一来,BPF map里的数据就可以被直接访问了,不需要频繁调用系统调用了。
在目前的patch set里面,只有array map可以采用这种方式进行映射。如果map里面包含spinlock的话,则不能这样映射(因为user space无法配合进行spinlock的操作)。这种map必须要在创建时使用BPF_F_MAPPABLE属性(这样kernel就知道可以对它们采用不同方式在内存中进行排布)才可以支持被映射使用。这个patch set已经合入BPF代码仓库了,在5.6 kernel里面应该就会包含。
Batched map operations
上面介绍的mempry-mapping BPF map是一种用来减少bpf()系统调用的方案,不过,上面也指出了它带有的限制。Brian Vazquez则提出了batched operations patch set,提供了另一个方案来减少系统调用。访问BPF map元素的时候,还是需要调用系统调用,不过可以支持在一次系统调用里面访问多个元素的数据了。
具体来说,这组patch set引入了4个用于操作map的新命令:BPF_MAP_LOOKUP_BATCH,BPF_MAP_LOOKUP_AND_DELETE_BATCH,BPF_MAP_UPDATE_BATCH,BPF_MAP_DELETE_BATCH。这些命令要求在调用bpf()的时候传入如下结构:
struct { /* struct used by BPF_MAP_*_BATCH commands */
__aligned_u64 in_batch;
__aligned_u64 out_batch;
__aligned_u64 keys;
__aligned_u64 values;
__u32 count;
__u32 map_fd;
__u64 elem_flags;
__u64 flags;
} batch;
如果是要进行LOOKUP操作(名字起得不好,这其实是遍历读取map里面的多个entry,而不是查找某个特定entry),keys成员会指向一个存放数量为count的key(键)数组,values数组则包含了数量为count的value(值)。kernel会遍历map,把这么多对key和对应的value都写入这两个数组,接着把count值改为真正返回的键值对的个数。如果in_batch被设置为NULL,那么就是从map的最开头来读取键值对,而out_batch值则可以用来基于上次读到的位置来继续向后读,就能把全部map都读取出来了。
Update和delete这两个操作要求keys成员里面包含所要操作的map成员。
利用这些批处理操作来访问map元素,并不是完全不需要进行系统调用了,不过确实可以大大减少系统调用的数量。一次系统调用就能操作100个(甚至更多的)元素,不像以前只能操作1个元素。批量操作比起memory-mapping方式来说确实有明显的一些优势,例如,可以用在任意类型的map上,不仅仅是数组类型的map。也可以支持一些mempry-mapping无法支持的操作(例如delete操作)。
其实两种方案都有可能都用得上。这组patch set已经来到了第3版,也有不少review意见和赞同意见了,所以看起来也会在近期合入mainline。
全文完
LWN文章遵循CC BY-SA 4.0许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注LWN深度文章以及开源社区的各种新近言论~