BPF的可移植性和CO-RE (Compile Once – Run Everywhere) (3)

编译并创建内核镜像,如果仅需要vmlinux的话,在编译完之后执行make vmlinux即可

$ make #可以使用多核方式加速编译,指定使用4个核 $ make -j 4 #使用nproc命令获取到的核数 $ make -j $(nproc)

安装内核:

$ sudo make modules_install

安装内核:

$ sudo make install

更新 grub config文件

$ sudo grub2-mkconfig -o /boot/grub2/grub.cfg $ sudo grubby --set-default /boot/vmlinuz-5.6.9

重启

通过上述命令可以获得到一个可兼容的C头文件(即"vmlinux.h"),包含所有的内核类型("所有"意味着包含那些不会通过kernel-devel包暴露的头文件)。

编译器支持

为了启用BPF CO-RE,并让BPF加载程序(即libbpf)将BPF程序调整为在目标主机上运行的特定内核,Clang扩展了一些内置功能,通过这些扩展功能可以发出BTF重定位,捕获有关BPF程序代码打算读取哪些信息的高级描述。例如要读取task_struct->pid字段,Clang会记录一个名为"pid"的字段,类型为"pid_t",位于struct task_struct中。这样,即使目标内核的task_struct结构中的"pid"字段在task_struct结构体内部发生了偏移(如,由于"pid"字段前面添加了额外的字段),或即使该字段转移到了某个嵌套的匿名结构或联合体中,这样也能够通过其名称和类型信息找到它。这种方式称为字段偏移量重定位

通过这种方式可以捕获不仅一个字段的偏移量,也可以捕获字段的其他属性,如字段的存在性或大小。即使对于比特字段(众所周知,它们是C语言中“拒绝合作”的数据),也能够捕获足够多的数据来使这些字段可重定位,所有这些对于BPF程序开发人员都是透明的。

BPF加载器(libbpf)

前面的所有数据最终会集合到一起,由libbpf进行处理,libbpf作为BPF程序的加载器。它会使用编译好的BPF ELF文件,必要时对其进行后处理,配置各种内核对象(maps,programs等),然后触发BPF程序的加载和验证。

libbpf知道如何将BPF程序代码匹配到特定的内核。它会查看程序记录的BTF类型和重定位信息,然后将这些信息与内核提供的BTF信息进行匹配。libbpf解析并匹配所有的类型和字段,更新必要的偏移以及重定位数据,确保BPF程序的逻辑能够正确地运行在特定的内核上。如果一切顺利,则BPF应用开发人员会获得一个BPF程序,这种方式可以针对目标主机上的内核进行“量身定制”,就好像程序是专门针对这个内核编译的,但无需在应用程序中分发Clang以及在目标主机上的运行时中执行编译,就可以实现所有这些目标。

内核

令人惊奇的是,内核无需太多变动就可以支持BPF CO-RE。归功于一个好的关注点分离(separation of concerns,SOC),当libbpf处理完BPF程序代码之后,在内核看来,它与其他有效的BPF程序代码一样,与使用最新内核头文件在主机上直接编译的BPF程序并没有区别,这意味着BPF CO-RE的许多功能都不需要先进的内核功能,因此可以更广泛,更迅速地进行调整。

有可能在某些场景下需求较新内核的支持,但这种情况很少。在下一部分中,我们将在解释BPF CO-RE面向用户的机制时讨论这种情况,其中将详细介绍BPF CO-RE面向用户的API。

BPF CO-RE:面向用户的体验

现在我们将看一下BPF应用的一些典型场景,以及如何通过BPF CO-RE解决兼容性问题。下面可以看到,一些可移植性问题(如兼容结构体布局差异)可以透明地进行处理,但其他一些场景则需要更加显示地处理,如if/else条件判断(与编译时BCC程序中的#ifdef/#else构造相反)和BPF CO-RE提供的一些额外机制。

摆脱对内核头文件的依赖

除了使用内核的BTF信息进行字段的重定位意外,还可以将BTF信息生成一个大(基于5.10.1版本生成的长度有106382行)的头文件("vmlinux.h"),其中包含了所有的内核内部类型,可以避免对系统范围的内核头文件的依赖。可以使用如下方式生成vmlinux.h:

$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

当使用了vmlinux.h,此时就不需要依赖像#include <linux/sched.h>, #include <linux/fs.h>这样的头文件,仅需要\#include "vmlinux.h"即可。该头文件包含了所有的内核类型:暴露了UAPI,通过kernel-devel提供的内部类型,以及其他一些更加内部的内核类型

不幸的是,BTF(即DWARF)不会记录#define宏,因此在vmlinux.h中丢失一些常用的宏。但大多数丢失的宏可以通过libbpf的bpf_helpers.h(即libbpf提供的内核侧的库)头文件提供。

读取内核结构体字段

大多数场景下会从某个内核结构中读取一个字段。假设我们期望读取task_struct结构体的pid字段。使用BCC时非常简单:

pid_t pid = task->pid;

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpfpjy.html