,其中给出了几个源于内核源码树中的例子,如果选择使用内核树中的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应用开发者应该能够预料到这个问题。考虑到不断变化的内核。但有时候需要访问原始的内核数据(如经常会访问到的 `struct task_struct`,表示一个进程或线程,包含大量进程信息),此时就只能靠自己了。跟踪,监视和分析应用程序通常是这种情况,这些应用程序是一类非常有用的BPF程序。 在这种情况下,如果某些内核在需要采集的字段(如从`struct task_struct`开始的第16个字节的偏移处)前添加了一个新的字段,那么此时如何保证不会读取到垃圾数据?如果一个字段重命名了又如何处理(如内核4.6和4.7的`thread_struct`的fs字段的名称是不同的)?或者如果需要基于一个内核的两种配置来运行程序,其中一个配置会禁用某些特性,并编译出部分结构(一种常见的场景是解释字段,这些字段是可选的,但如果存在则非常有用)?所有这些条件意味着无法使用本地开发服务器上的头文件编译出一个BPF程序,然后分发到其他系统上运行。这是因为不同内核版本的头文字中的数据的内存布局可能是不同的。 迄今为止,人们编译这类BPF程序会依赖[BCC](
) (BPF Compiler Collection)。使用BCC,可以将BPF程序的C代码以字符串的形式嵌入到用户空间的程序中,当程序最终部署并运行在目标主机上后,BCC会唤醒其嵌入的Clang/LLVM,提取本地内核头文件(必须确保已从正确的kernel-devel软件包中将其安装在系统上),并即时进行编译。通过这种方式来确保BPF程序期望的内存布局和主机运行的内核的内存布局是相同的。如果需要处理一些选项和内核编译出来的潜在产物,则可以在自己的源代码中添加`#ifdef`/`#else`来适应重命名字段、不同的数值语义或当前配置导致的不可用内容等带来的风险。嵌入的Clang会移除代码中无关的内容,并调整BPF程序代码,以匹配到特定的内核。 这种方式听起来很不错,但实际并非没有缺点: - Clang/LLVM组合是一个很大的库,导致发布的应用的库会比较大 - Clang/LLVM组合使用的资源比较多,因此当编译的BPF代码启动时会消耗大量资源,可能会推翻已均衡的生产负载 - 这样做其实也是在赌目标系统将存在内核头文件,大多数情况下这不是问题,但有时可能会引起很多麻烦。这也是内核开发人员感到特别麻烦的要求,因为他们经常必须在开发过
