libbpf 概述¶
libbpf 是一个基于 C 语言的库,包含一个 BPF 加载器,用于接收编译后的 BPF 目标文件并准备将其加载到 Linux 内核中。libbpf 承担了加载、验证和将 BPF 程序附加到各种内核钩子的繁重工作,使 BPF 应用程序开发人员能够专注于 BPF 程序的正确性和性能。
以下是 libbpf 支持的高级功能
为用户空间程序提供高级和低级 API,以便与 BPF 程序交互。低级 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 钩子点(例如,tracepoints、kprobes、cgroup 钩子、网络数据包处理管道等)。在此阶段,BPF 程序执行有用的工作,例如处理数据包,或更新可以从用户空间读取的 BPF 映射和全局变量。
拆卸阶段:在拆卸阶段,libbpf 分离 BPF 程序并将其从内核中卸载。BPF 映射被销毁,并且 BPF 应用程序使用的所有资源都被释放。
BPF 对象骨架文件¶
BPF 骨架是 libbpf API 的另一种接口,用于处理 BPF 对象。骨架代码抽象了通用的 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 (Compile Once – Run Everywhere)¶
BPF 程序在内核空间中运行,可以访问内核内存和数据结构。BPF 应用程序遇到的一个限制是缺乏跨不同内核版本和配置的可移植性。BCC 是 BPF 可移植性的解决方案之一。然而,它带来了运行时开销,并且由于将编译器嵌入到应用程序中而导致二进制文件大小庞大。
libbpf 通过支持 BPF CO-RE 概念来提高 BPF 程序的可移植性。BPF CO-RE 将 BTF 类型信息、libbpf 和编译器结合在一起,生成一个可以在多个内核版本和配置上运行的单一可执行二进制文件。
为了使 BPF 程序可移植,libbpf 依赖于运行中内核的 BTF 类型信息。内核还通过 sysfs
在 /sys/kernel/btf/vmlinux
暴露这种自描述的权威 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 字段读入 parent_task
变量。bpf_core_read()
就像 bpf_probe_read_kernel()
BPF 助手一样,不同之处在于它记录了关于应在目标内核上重定位的字段的信息。即,如果 parent
字段由于在其前面添加了一些新字段而在 struct task_struct
内移动到不同的偏移量,libbpf 将自动将实际偏移量调整为正确的值。
libbpf 入门¶
查看 libbpf-bootstrap 仓库,其中包含使用 libbpf 构建各种 BPF 应用程序的简单示例。
另请参阅 libbpf API 文档。
libbpf 和 Rust¶
如果您正在使用 Rust 构建 BPF 应用程序,建议使用 Libbpf-rs 库,而不是直接绑定 libbpf。Libbpf-rs 将 libbpf 功能封装在 Rust 风格的接口中,并提供 libbpf-cargo 插件来处理 BPF 代码编译和骨架生成。使用 Libbpf-rs 将使构建 BPF 应用程序的用户空间部分变得更容易。请注意,BPF 程序本身仍必须用纯 C 编写。
libbpf 日志记录¶
默认情况下,libbpf 将信息和警告消息记录到 stderr。可以通过将环境变量 LIBBPF_LOG_LEVEL 设置为 warn、info 或 debug 来控制这些消息的详细程度。可以使用 libbpf_set_print()
设置自定义日志回调。