BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
在上一篇文章中介绍了提高socket性能的几个socket选项,其中给出了几个源于内核源码树中的例子,如果选择使用内核树中的Makefile进行编译的话,可能会出现与本地头文件冲突的情况,如重复定义变量,结构体类型不对等错误。这些问题大大影响了BPF程序的可移植性。
本文将介绍BPF可移植性存在的问题,以及如何使用BPF CO-RE(Compile Once – Run Everywhere)解决这些问题。
BPF:最前沿的技术自BPF成立以来,BPF社区将尽可能简化BPF应用程序的开发作为工作重点,目的是将BPF的使用变得与用户空间的应用一样简单明了。伴随着BPF可编程性的稳步发展,BPF程序的开发也越来越简单。
尽管BPF提升了使用上的便利性,但却忽略了BPF程序开发中的一个方面:可移植性。"BPF可移植性"意味着什么?我们将BPF可移植性定义为成功编写并通过内核验证的一个BPF程序,且跨内核版本可用,无需针对特定的内核重新编译。
本文描述了BPF的可移植性问题以及解决方案:BPF CO-RE(Compile Once – Run Everywhere)。首先会调研BPF本身的可移植性问题,描述为什么这是个问题,以及为什么解决它很重要。然后,我们将介绍解决方案中的高级组件:BPF CO-RE,并简要介绍实现这一目标所需要解决的难题。最后,我们将以各种教程作为结尾,介绍BPF CO-RE方法的用户API,并提供相关示例。
BPF可移植性的问题BPF程序是用户提供的一部分代码,这些代码会直接注入到内核,一旦经过加载和验证,BPF程序就可以在内核上下文中运行。这些程序运行在内核的内存空间中,并能够访问所有可用的内核内部状态,这种功能非常强大,这也是为什么BPF技术成功落地到多个应用中的原因。然而,在使用其强大的能力的同时也带来了一些负担:BPF程序无法控制周围内核环境的内存布局,因此必须依赖独立的开发,编译和部署的内核。
此外,内核类型和数据结构会不断变化。不同的内核版本会在结构体内部混用结构体字段,甚至会转移到新的内部结构体中。结构体中的字段可能会被重命名或删除,类型可能会改变(变为微兼容或完全不同的类型)。结构体和其他类型可以被重命名,被条件编译(取决于内核配置),或直接从内核版本中移除。
换句话讲,不同内核发布版本中的所有内容都有可能发生变化,BPF应用开发者应该能够预料到这个问题。考虑到不断变化的内核环境,那么该如何利用BPF做有用的事?有如下几点原因:
首先,并不是所有的BPF程序都需要访问内部的内核数据结构。一个例子是opensnoop工具,该工具依靠kprobes /tracepoints来跟踪哪个进程打开了哪些文件,仅需要捕获少量的系统调用就可以工作。由于系统调用提供了稳定的ABI,不会随着内核版本而变化,因此不用考虑这类BPF程序的可移植性。不幸的是,这类应用非常少,且这类应用的功能也大大受限。
此外,内核内部的BPF机器提供了有限的“稳定接口”集,BPF程序可以依靠这些稳定接口在内核间保持稳定。事实上,不同版本的内核的底层结构和机制是会发生变化的,但BPF提供的稳定接口从用户程序中抽象了这些细节。
例如,网络应用会通过查看少量的sk_buff(即报文数据)中的属性来获得非常有用且通用的信息。为此,BPF校验器提供了一个稳定的__sk_buff 视图(注意前面的下划线),该视图为BPF程序屏蔽了struct sk_buff结构体的变更。所有对__sk_buff字段访问都可以透明地重写为对实际sk_buff的访问(有时非常复杂-在获取最终请求的字段之前需要追踪一堆内部指针)。类似的机制同样适用于不同的BPF程序类型,通过BPF校验器来识别特定类型的BPF上下文。如果使用这类上下文开发BPF程序,就可以不用担心可移植性问题。
但有时候需要访问原始的内核数据(如经常会访问到的 struct task_struct,表示一个进程或线程,包含大量进程信息),此时就只能靠自己了。跟踪,监视和分析应用程序通常是这种情况,这些应用程序是一类非常有用的BPF程序。
在这种情况下,如果某些内核在需要采集的字段(如从struct task_struct开始的第16个字节的偏移处)前添加了一个新的字段,那么此时如何保证不会读取到垃圾数据?如果一个字段重命名了又如何处理(如内核4.6和4.7的thread_struct的fs字段的名称是不同的)?或者如果需要基于一个内核的两种配置来运行程序,其中一个配置会禁用某些特性,并编译出部分结构(一种常见的场景是解释字段,这些字段是可选的,但如果存在则非常有用)?所有这些条件意味着无法使用本地开发服务器上的头文件编译出一个BPF程序,然后分发到其他系统上运行。这是因为不同内核版本的头文字中的数据的内存布局可能是不同的。