libbpf 概述¶
libbpf 是一个基于 C 的库,包含一个 BPF 加载器,它接受已编译的 BPF 对象文件,并准备并将它们加载到 Linux 内核中。libbpf 承担了加载、验证以及将 BPF 程序附加到各种内核钩子的繁重工作,允许 BPF 应用程序开发人员只专注于 BPF 程序的正确性和性能。
以下是 libbpf 支持的高级功能:
为用户空间程序提供与 BPF 程序交互的高级和低级 API。低级 API 封装了所有 bpf 系统调用功能,当用户需要对用户空间和 BPF 程序之间的交互进行更精细的控制时,这些功能非常有用。
为 bpftool 生成的 BPF 对象骨架提供全面支持。骨架文件简化了用户空间程序访问全局变量和使用 BPF 程序的过程。
提供 BPF 端 API,包括 BPF 助手定义、BPF 映射支持和跟踪助手,使开发人员能够简化 BPF 代码编写。
支持 BPF CO-RE 机制,使 BPF 开发人员能够编写可移植的 BPF 程序,这些程序可以编译一次并在不同的内核版本上运行。
本文档将详细深入探讨上述概念,提供对 libbpf 的功能和优势以及它如何帮助您高效开发 BPF 应用程序的更深入理解。
BPF 应用程序生命周期和 libbpf API¶
一个 BPF 应用程序由一个或多个 BPF 程序(协作或完全独立)、BPF 映射和全局变量组成。全局变量在所有 BPF 程序之间共享,这使它们可以在一组公共数据上进行协作。libbpf 提供用户空间程序可以用来通过触发 BPF 应用程序生命周期的不同阶段来操作 BPF 程序的 API。
以下部分简要概述了 BPF 生命周期中的每个阶段:
打开阶段:在此阶段,libbpf 解析 BPF 对象文件并发现 BPF 映射、BPF 程序和全局变量。打开 BPF 应用程序后,用户空间应用程序可以在创建和加载所有实体之前进行其他调整(如果需要,设置 BPF 程序类型;预设全局变量的初始值等)。
加载阶段:在加载阶段,libbpf 创建 BPF 映射、解析各种重定位、验证并将 BPF 程序加载到内核中。此时,libbpf 验证 BPF 应用程序的所有部分,并将 BPF 程序加载到内核中,但尚未执行任何 BPF 程序。在加载阶段之后,可以在不与 BPF 程序代码执行竞争的情况下设置初始 BPF 映射状态。
附加阶段:在此阶段,libbpf 将 BPF 程序附加到各种 BPF 钩子点(例如,跟踪点、kprobes、cgroup 钩子、网络数据包处理管道等)。在此阶段,BPF 程序执行有用的工作,例如处理数据包或更新 BPF 映射和全局变量,这些映射和全局变量可以从用户空间读取。
拆卸阶段:在拆卸阶段,libbpf 分离 BPF 程序并将它们从内核中卸载。销毁 BPF 映射,并释放 BPF 应用程序使用的所有资源。
BPF 对象骨架文件¶
BPF 骨架是用于处理 BPF 对象的 libbpf API 的替代接口。骨架代码抽象出通用的 libbpf API,以显著简化从用户空间操作 BPF 程序的代码。骨架代码包括 BPF 对象文件的字节码表示,简化了分发 BPF 代码的过程。通过嵌入的 BPF 字节码,无需额外文件与应用程序二进制文件一起部署。
您可以通过将 BPF 对象传递给 bpftool,为特定对象文件生成骨架头文件 (.skel.h)
。生成的 BPF 骨架提供以下与 BPF 生命周期相对应的自定义函数,每个函数都以特定对象名称为前缀:
<name>__open()
– 创建和打开 BPF 应用程序(<name>
代表特定的 bpf 对象名称)<name>__load()
– 实例化、加载和验证 BPF 应用程序部件<name>__attach()
– 附加所有可自动附加的 BPF 程序(它是可选的,您可以通过直接使用 libbpf API 来获得更多控制)<name>__destroy()
– 分离所有 BPF 程序并释放所有使用的资源
使用骨架代码是处理 bpf 程序的推荐方法。请记住,BPF 骨架提供对底层 BPF 对象的访问,因此即使在使用 BPF 骨架时,仍然可以使用通用 libbpf API 执行任何操作。它是一种附加的便利功能,没有系统调用,也没有繁琐的代码。
使用骨架文件的其他优点¶
BPF 骨架为用户空间程序提供了一个与 BPF 全局变量交互的接口。骨架代码将全局变量作为结构体内存映射到用户空间中。结构体接口允许用户空间程序在 BPF 加载阶段之前初始化 BPF 程序,并在之后从用户空间获取和更新数据。
skel.h
文件通过列出可用的映射、程序等来反映对象文件结构。BPF 骨架提供对所有 BPF 映射和 BPF 程序的直接访问,作为结构体字段。这消除了使用bpf_object_find_map_by_name()
和bpf_object_find_program_by_name()
API 进行基于字符串的查找的需要,减少了由于 BPF 源代码和用户空间代码不同步而导致的错误。对象文件的嵌入式字节码表示确保骨架和 BPF 对象文件始终同步。
BPF 助手¶
libbpf 提供 BPF 端 API,BPF 程序可以使用这些 API 与系统交互。BPF 助手定义允许开发人员在 BPF 代码中像使用其他普通 C 函数一样使用它们。例如,有一些助手函数可以打印调试消息、获取自系统启动以来的时间、与 BPF 映射交互、操作网络数据包等。
有关助手执行的操作、它们接受的参数以及返回值的完整描述,请参阅 bpf-helpers 手册页。
BPF CO-RE(一次编译 - 到处运行)¶
BPF 程序在内核空间中工作,并且可以访问内核内存和数据结构。BPF 应用程序遇到的一个限制是缺乏跨不同内核版本和配置的可移植性。BCC 是 BPF 可移植性的解决方案之一。但是,它带来了运行时开销和来自将编译器嵌入到应用程序中的大型二进制文件。
libbpf 通过支持 BPF CO-RE 概念来提升 BPF 程序的便携性。BPF CO-RE 将 BTF 类型信息、libbpf 和编译器结合在一起,生成一个可在多个内核版本和配置上运行的单个可执行二进制文件。
为了使 BPF 程序可移植,libbpf 依赖于正在运行的内核的 BTF 类型信息。内核还通过 /sys/kernel/btf/vmlinux
中的 sysfs
公开此自我描述的权威 BTF 信息。
您可以使用以下命令为正在运行的内核生成 BTF 信息:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
该命令生成一个 vmlinux.h
头文件,其中包含正在运行的内核使用的所有内核类型 (BTF 类型)。在 BPF 程序中包含 vmlinux.h
可消除对系统范围的内核头的依赖。
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()
将任务结构的父字段读取到 parent_task
变量中。bpf_core_read()
类似于 bpf_probe_read_kernel()
BPF 助手,只是它会记录关于目标内核上应重新定位的字段的信息。也就是说,如果由于在 struct task_struct
前面添加了一些新字段,导致 parent
字段的偏移量发生了变化,libbpf 将会自动将实际偏移量调整为正确的值。
libbpf 入门¶
请查看 libbpf-bootstrap 仓库,其中包含使用 libbpf 构建各种 BPF 应用程序的简单示例。
另请参阅 libbpf API 文档。
libbpf 和 Rust¶
如果您正在使用 Rust 构建 BPF 应用程序,建议使用 Libbpf-rs 库,而不是直接使用 bindgen 绑定到 libbpf。Libbpf-rs 以 Rust 惯用接口封装 libbpf 功能,并提供 libbpf-cargo 插件来处理 BPF 代码编译和骨架生成。使用 Libbpf-rs 将使 BPF 应用程序的用户空间部分的构建更加容易。请注意,BPF 程序本身仍然必须用纯 C 编写。
libbpf 日志记录¶
默认情况下,libbpf 将信息和警告消息记录到 stderr。可以通过将环境变量 LIBBPF_LOG_LEVEL 设置为 warn、info 或 debug 来控制这些消息的详细程度。可以使用 libbpf_set_print()
设置自定义日志回调。