本章是 libbpf 文档的中文,其中会穿插一些个人的理解,还有一些在开发中会用到的名词,我会通过*译文(原文)*的方式去描述。
libbpf 概述
libbpf 是一个包含 BPF 加载器的基于C语言的库,它获取一个已编译的 BPF 对象文件,并预准备加载它们到Linux内核中。libbpf
承担了包括加载、验证、附加 BPF程序到各种内核钩子处的繁重的步骤,使 BPF 应用程序的开发者只需要关注 BPF 程序的逻辑与性能问题。
下面是 libbpf 支持的高级特性:
- 提供了高级(high-level)和低级(low-level)的API给用户空间的程序去和BPF程序进行交互。低级的API封装了全部的 bpf 系统调用方法,当用户需要在用户空间和BPF程序之间进行更细粒度的交互式这是很有用的。
- 为
bpftool
生成的 BPF 对象框架提供了一个全方面的支持。skeleton
文件简化了用户空间程序访问全局变量和使用 BPF 程序的过程。 - 提供了 BPF 端的API,包含 BPF Helper Funcs、BPF Maps支持以及跟踪助手,允许开发者去简化 BPF 代码编写。
- 支持 BPF CO-RE(Compile Once-Run Everywhere) 机制,使 BPF 开发者能够编写可移植的 BPF 程序,能够编译一次在不同的内核版本中运行。
本文将详细研究上述概念,深入了解libbpf的功能和优势,以及它如何帮助您高效地开发BPF应用程序。
BPF 应用程序的生命周期和 libbfp API
一个BPF应用由一个或多个 BPF程序组成(要么组合使用要么独立使用),BPF Maps和全局变量。全局变量在全部的 BPF 程序之间共享,这允许多个BPF程序通过通用数据集进行一个合作。libbpf 提供了用户空间程序能够使用的API,通过触发BPF程序生命周期的不同阶段来操纵 BPF 程序(下文会讲到)。
下面的章节提供了对BPF生命周期中每个阶段的概述:
-
Open阶段:在这个阶段,libbpf 解析 BPF 对象文件,发现 BPF Maps、BPF 程序和全局变量。在一个 BPF 应用被打开后,用户空间的应用能够在全部的入口点(解析出来的对象)被创建和载入之前去进行一些额外的调整(必要时设置 BPF 程序类型、预设一个全局变量的初始化值等)。
-
Load阶段:在加载阶段, libbpf 创建 BPF Maps、解决各种重定位、以及验证和加载 BPF 程序到内核。在这个点,libbpf验证BPF应用程序的所有部分,并将BPF程序加载到内核中,但是到目前位置,还没有 BPF 程序被执行。在加载阶段之后,还可以设置初始BPF Maps状态,而不必与BPF程序代码执行赛跑。一步一步来,因为如果BPF程序执行时出现未初始化的Maps会报错,所以我们可以先初始化Maps,再让 BPF 程序运行起来。
-
Attach阶段:在这个阶段,libbpf 附加 BPF 程序到各种 BPF Hook 点(例如,
tracepoints
、kprobes
、cgroup hooks
、network packet processing pipeline
等)。在这个阶段期间,BPF 程序执行像处理数据包或者更新可以从用户空间读取 BPF Maps和全局变量等工作。 -
Tear Down阶段:在拆除阶段,libbpf 分离 BPF 程序并从内核中卸载它们。BPF 映射被销毁,BPF应用程序使用的所有资源都被释放。
以上就是 BPF 应用的生命周期,从上文我们可以得知一个BPF应用可以由多个BPF程序组成,我们可以把这些 BPF 程序组合使用也可以单独使用:
- 组合使用:需要用到 Tail calls
- 单独使用即在用户态程序中去调用即可。
BPF 对象框架(Skeleton)文件
BPF 框架 (Skeleton) 是 libbpf API 的一种替代接口,用于操作 BPF 对象。在下面的内容中,我将会采用 libbpf
的缩写方式去简写框架这个名词。统一采用 skel
去表示。
skel 代码抽象了通用的 libbpf API,从而显著简化了从用户空间操作 BPF 程序的代码。skel 代码包含 BPF 对象文件的字节码表示,简化了 BPF 代码的分发过程。由于 BPF 字节码被嵌入其中,因此在部署应用程序二进制文件时无需额外的文件。
生成的BPF框架提供了以下与BPF生命周期相对应的自定义函数,每个函数都以特定的对象名称为前缀:
<name>__open()
:创建和打开一个 BPF 应用程序(<name>
代表了特定的 BPF 对象名)。<name>__loca()
:实例化、加载和验证 BPF 应用程序部分。<name>__attach()
:附加全部的可自动附加的程序(这是可选的,你可以直接使用 libbpf API 来进行更多的控制)。<name>__destroy()
:分离全部的BPF程序并释放全部被使用的资源。
使用框架代码是使用 bpf 程序推荐的方式。记住,BPF 框架提供了对 BPF 对象的底层访问,因此,即使使用了BPF框架,使用通用libbpf api所能做的一切依然是可以的。这只是一个额外的便利功能,没有syscall也没有麻烦的代码。
使用 Skeleton 的其它优点
- BPF 框架提供了一个对于用户空间去和 BPF 全局变量工作的接口。这个框架代码将全局变量通过一个结构体以内存映射的方式到用户空间。这个结构体结构允许用户空间程序在这个 BPF Load 阶段之前初始化 BPF 程序,然后从用户空间获取和更新数据。
skel.h
文件通过列出可用的Maps和程序等去反映对象文件的结构。BPF 框架提供了访问结构体字段直接去访问全部 BPF Maps 和 BPF 程序的能力。这消除了需要bpf_object_find_map_by_name()
和bpf_object_find_program_by_name()
API去进行基于字符串查询的必要,减少由于BPF源代码和用户空间代码不同步而导致的错误。
BPF Helpers
libbpf 提供了 BPF 方面的 API,BPF 能够使用它去和系统进行交互。BPF helper的定义允许开发人员在BPF代码中像使用其他普通C函数一样使用它们。例如,有打印调试信息的helper,获取系统启动时间的helper和 BPF Maps 交互的、操作网络包的,以上等等。
有关帮助程序所做的工作、它们接受的参数和返回值的完整描述,请参阅bpf-helpers手册页。
BPF CO-CE
BPF程序在内核空间中工作,可以访问内核内存和数据结构。BPF应用程序遇到的一个限制是缺乏跨不同内核版本和配置的可移植性。BCC是一个可移植BPF解决方案之一。但是它对Python运行环境重度依赖,对于某些功能的调试还是可以。
但如果是放在生产环境中,我们应该是一个部署即用的,而不是需要去部署环境。当然,我们也可以将BCC应用程序与编译器进行打包,由于将编译器嵌入到应用程序中,它带来了运行时开销和较大的二进制文件大小。
libpf通过支持BPF CO-RE概念来提高BPF程序的可移植性。BPF CO-RE将BTF类型信息、libbpf和编译器结合在一起,生成可以在多个内核版本和配置上运行的单个可执行二进制文件。
为了使BPF程序可移植,libbpf依赖于运行内核的BTF类型信息。Kernel还通过sysfs在/sys/kernel/btf/vmlinux
上公开这种自描述的权威BTF信息。
你可以通过下面的命令去生成当前运行内核的 BTF 信息:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
这个命令生成了一个 vmlinux.h
拥有当前运行内核的全部内核类型的头文件。include vmlinux.h
在你的 BPF 程序,消除对系统范围内核头文件的依赖。
libbpf通过查看BPF程序记录的BTF类型和重定位信息,并将它们与运行内核提供的BTF信息(vmlinux)进行匹配,从而实现BPF程序的可移植性。libbpf 然后解析和匹配全部的类型和字段,更新必要的偏移和其它的可重定位数据,以确保BPF程序的逻辑在主机上的特定内核中正确运行。因此,BPF CO-RE概念消除了与BPF开发相关的开销,并允许开发人员编写可移植的BPF应用程序,而无需修改和在目标机器上运行时编译源代码。
下面的代码片段展示了如何使用BPF CO-RE和libbf读取内核task_struct
的父字段。以CO-RE可重定位方式读取字段的基本助手是bpf_core_read(dst, sz, src)
,它将从src
引用的字段中读取sz
字节到dst
指向的内存中。
//...
struct task_struct *task = (void *)bpf_get_current_task();
struct task_struct *parent_task;
int err;
err = bpf_core_read(&parent_task, sizeof(void *), &task->parent);
if (err) {
/* handle error */
}
/* parent_task contains the value of task->parent pointer */
在代码片段中,我们使用bpf_get_current_task()
方法去获取了指向当前task_struct
的指针。我们然后使用 bpf_core_read()
去读取task
结构体的parent
字段到 parent_task
变量。bpf_core_read()
就像 bpf_probe_read_kernel()
BPF Helper 一样,除了它记录有关应该在目标内核上重新定位的字段的信息。也就是说,如果parent
字段在struct task_struct
中由于前面添加了一些新字段而被转移到不同的偏移量,libbpf将自动将实际偏移量调整为适当的值。
开始使用 libbpf
查看 libbpf-bootstrap仓库用一些简单的案例使用 libbpf 去构建各种 BPF 应用程序。
也可以查看libbpf API 文档