文章目录
译者序
翻译自:Building BPF applications with libbpf-bootstrap
本篇之前,我的准备工作:一步步了解Argp 、 BPF应用-跟踪新进程的产生
使用libbpf-bootstrap脚手架快速轻松地开始使用您自己的 BPF 应用程序 ,它负责所有平凡的设置步骤,让您直接体验BPF 的乐趣并最小化必要的样板。我们将看看 libbpf-bootstrap 提供了什么以及所有内容如何联系在一起。
为什么是 libbpf-bootstrap?
BPF 是一项了不起的内核技术,它允许任何人在没有丰富的内核开发经验和花费大量时间为内核开发进行设置的情况下,选择内核如何运行。BPF 还消除了在执行此操作时使操作系统崩溃的风险。一旦你掌握了 BPF 的速度,它就会在你的手中充满乐趣和力量。
但是开始使用 BPF 在很大程度上仍然令人生畏,因为即使为一个简单的“Hello, World”式 BPF 应用程序设置构建工作流也需要一系列步骤,这些步骤可能会让新的 BPF 开发人员感到沮丧和害怕。这并不是那么复杂,但了解必要的步骤是一个(不必要的)困难的部分,这可能会使很多人甚至不愿尝试,尽管 BPF 的所有兴趣和承诺。(译者:确实如此,我花了很长时间在运行第一个bpf程序)
libbpf-bootstrap是一个脚手架操场,为初学者用户设置尽可能多的东西,让他们直接开始编写 BPF 程序并修改它们,而无需对初始设置造成不必要的挫折。它考虑了过去几年 BPF 社区开发的最佳实践,并提供了一个现代且方便的工作流程,可以说是迄今为止最好的 BPF 用户体验。libbpf-bootstrap 依赖于libbpf 并使用一个简单的 Makefile。对于需要更高级设置的用户来说,这应该是一个很好的起点。至少,如果不能直接使用 Makefile,那么只需将逻辑转移到需要使用的任何构建系统即可。
libbpf-bootstrap 目前有两个演示 BPF 应用程序可用:minimal
和bootstrap
. minimal
就是这样——编译、加载和运行一个简单的 BPF 等价物的最小 BPF 应用程序printf("Hello, World!")
。作为最小的一个,它也没有对 Linux 内核的最新性强加很多要求,并且应该在相当旧的内核版本上运行良好。
minimal
非常适合在本地进行快速实验和尝试,但它的设置并不反映可跨各种内核部署的基于生产意图的基于 BPF 的应用程序的设置。bootstrap
就是这样一个例子。bootstrap
演示展示了一种构建最小但功能齐全且可移植的 BPF 应用程序的真实方法。为此,它确实依赖于BPF CO-RE 和内核BTF支持,因此请确保您的 Linux 内核是使用CONFIG_DEBUG_INFO_BTF=y
Kconfig 构建的。有关 已为您设置好所有内容的 Linux 发行版列表,请参阅libbpf README。如果您想尽量减少构建自定义内核的麻烦,只需坚持使用任何主要 Linux 发行版的最新版本即可。
此外,bootstrap
演示了 BPF 全局变量的使用(Linux 5.5+)和BPF 环形缓冲区的使用(Linux 5.8+)。这些特性都不是构建有用的 BPF 应用程序所必需的,但它们带来了巨大的可用性改进,并且是构建现代 BPF 应用程序的方式,因此我在一个基本bootstrap
示例中添加了使用它们的示例。
先决条件
BPF 是一种非常动态的技术,它在不断地发展和演变。这意味着新的特性和功能一直在添加,因此根据您需要的功能,您可能需要更新的内核版本。但是 BPF 社区非常重视向后兼容性,这意味着旧的 Linux 内核仍然可以很好地运行 BPF 应用程序,前提是您不需要最新的功能集。因此,您的 BPF 应用程序逻辑和功能集越简单和保守,您就越有可能在旧内核上运行 BPF 应用程序。
话虽如此,BPF 用户体验一直在变好,而且 BPF 在更新的内核版本中提供了 BPF 可用性的深刻改进,所以如果你刚刚开始并且没有严格的要求来支持过时的 Linux 内核版本,请让你的减少痛苦,使用最新的内核版本,你可以得到你的手。
BPF 程序代码通常用 C 语言编写,并添加了一些代码组织约定,以使libbpf 理解 BPF 代码结构并将所有内容正确加载到内核中。Clang是用于 BPF 代码编译的编译器,一般建议尽可能使用最新的 Clang。尽管如此,Clang 10 或更新版本应该适用于大多数 BPF 功能,但一些更高级的BPF CO-RE功能可能需要 Clang 11 甚至 12(例如,对于一些最近和更高级的 CO-RE 重定位内置插件)。
libbpf-bootstrap 与 libbpf(作为 Git 子模块)和 bpftool(仅适用于 x86-64 架构)捆绑在一起,以避免依赖 Linux 发行版中可用的任何特定(并且可能已过时)版本。您的系统还应安装zlib
(libz-dev
或zlib-devel
包) 和libelf
(libelf-dev
或elfutils-libelf-devel
包) 。这些是libbpf
正确编译和运行它所必需的依赖项。
这不是 BPF 技术本身的入门,因此假设对 BPF 程序、BPF 映射、BPF 钩子(附加点)等基本概念有一定的了解。如果您需要复习 BPF 基础知识,这些 资源 应该是一个很好的起点。
在本文的其余部分,我将带您了解libbpf-bootstrap的结构 、它的 Makefile 以及两者minimal
和bootstrap
示例。我们将研究 libbpf 约定和构建 BPF C 代码以与 libbpf 一起用作 BPF 程序加载器,以及如何使用 libbpf API 从用户空间与您的 BPF 程序进行交互。
Libbpf-bootstrap 概述
这是libbpf-bootstrap
存储库的内容:
$ tree
.
├── libbpf
│ ├── ...
│ ...
├── LICENSE
├── README.md
├── src
│ ├── bootstrap.bpf.c
│ ├── bootstrap.c
│ ├── bootstrap.h
│ ├── Makefile
│ ├── minimal.bpf.c
│ ├── minimal.c
│ ├── vmlinux_508.h
│ └── vmlinux.h -> vmlinux_508.h
└── tools
├── bpftool
└── gen_vmlinux_h.sh
16 directories, 85 files
libbpf-bootstrap
将 libbpf 捆绑为子目录中的子模块,libbpf/
以避免依赖于系统范围的 libbpf 可用性和版本。
tools/
包含bpftool
二进制文件,用于构建 BPF 代码的BPF skeleton。与 libbpf 类似,它被捆绑以避免依赖于系统范围的 bpftool 可用性及其足够最新的版本。
此外,bpftool 可用于生成您自己的vmlinux.h
包含所有 Linux 内核类型定义的头文件。您可能不需要这样做,因为 libbpf-bootstrap 已经 在子目录中提供了预先生成的 vmlinux.hsrc/
。它基于 Linux 5.8 的默认内核配置,并启用了一堆额外的 BPF 相关功能。这意味着它应该已经有很多常用的内核类型和常量。由于BPF CO-RE,vmlinux.h
不必完全匹配您的内核配置和版本。但是,如果您确实需要生成自定义vmlinux.h
,请随时检查 tools/gen_vmlinux_h.sh
脚本以了解如何完成。
Makefile 定义了必要的构建规则来编译所有提供的(和你自定义的)BPF 应用程序。它遵循一个简单的文件命名约定:
<app>.bpf.c
文件是包含要在内核上下文中执行的逻辑的 BPF C 代码;<app>.c
是用户空间的 C 代码,它在应用程序的整个生命周期中加载 BPF 代码并与之交互;- optional
<app>.h
是具有通用类型定义的头文件,由应用程序的 BPF 和用户空间代码共享。
因此,minimal.c
并minimal.bpf.c
形成minimal
BPF 演示应用程序。和 bootstrap.c
、bootstrap.bpf.c
和bootstrap.h
是bootstrap
BPF 应用程序。简单的。
Minimal app
minimal
是一个很好的例子。将其视为尝试 BPF 事物的简约游乐场。它不使用 BPF CO-RE,因此您可以使用较旧的内核,只需包含用于内核类型定义的系统内核头文件。这不是构建生产就绪应用程序和工具的最佳方法,但对于本地实验来说已经足够了。
The BPF side
这是 BPF 端代码(minimum.bpf.c) 的全部内容:
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
int my_pid = 0;
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\n", pid);
return 0;
}
#include <linux/bpf.h>
包括一些基本的 BPF 相关类型和使用内核端 BPF API 所需的常量(例如,BPF 辅助函数标志)。标题需要此bpf_helpers.h
标题,接下来包含。 bpf_helpers.h
由libbpf
最常用的宏、常量和 BPF 帮助器定义提供并包含这些定义,几乎每个现有的 BPF 应用程序都使用它们。bpf_get_current_pid_tgid()
上面是这种 BPF 助手的一个例子。
LICENSE
变量定义了 BPF 代码的许可。指定许可证是强制性的,由内核强制执行。某些 BPF 功能对非 GPL 兼容代码不可用。注意特殊SEC("license")
注释。SEC()
(由 提供bpf_helpers.h
)将变量和函数放入指定的部分。SEC("license")
,以及其他一些部分名称,是由 规定的约定libbpf
,因此请确保您坚持使用它。
接下来,我们将看到一个令人兴奋的 BPF 特性的使用:全局变量。int my_pid = 0;
完全符合您的期望:它定义了一个全局变量,BPF 代码可以读取和更新该变量,就像任何用户空间 C 代码对全局变量所做的一样。使用 BPF 全局变量来维护 BPF 程序的状态非常方便且高效。此外,可以从用户空间端读取和写入此类全局变量。此功能从 Linux 5.5 版本开始可用。它经常用于使用额外设置、低开销统计等配置 BPF 应用程序。它还可以用于在内核 BPF 代码和用户空间控制代码之间来回传递数据。
SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { ... }
定义将加载到内核中的 BPF 程序。它在特殊命名的部分(使用SEC()
宏)中表示为普通的 C 函数。节名称定义了应该创建什么类型的 BPF 程序 libbpf 以及它可以附加到内核中的方式/位置。在这种情况下,我们定义了一个跟踪点 BPF 程序,每次write()
从任何用户空间应用程序调用系统调用时都会调用该程序。
在同一个 BPF C 代码文件中可以定义许多 BPF 程序。它们可以有不同的类型(即
SEC()
注释)。例如,您可以有几个不同的 BPF 程序,每个程序用于不同的跟踪点或其他一些内核事件(例如,正在处理的网络数据包等)。您还可以定义多个具有相同SEC()
属性的BPF 程序:libbpf
将处理得很好。在同一个 BPF C 代码文件中定义的所有 BPF 程序共享所有全局状态(如my_pid
变量,还有任何 BPF 映射,如果使用的话)。这经常用于协调少数协作 BPF 程序。
现在让我们看看handle_tp
BPF 程序在做什么:
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
这部分获取以bpf_get_current_pid_tgid()
的返回值的高 32 位编码的 PID(或内部内核术语中的“TGID”)。然后它检查触发write()
系统调用的minimal
进程是否是我们的进程。这在繁忙的系统上非常重要,因为很可能很多不相关的进程都会发出write()
s,这使得按照自己的方式试验自己的 BPF 代码真的很困难。my_pid
全局变量将使用minimal
以下用户空间代码中的进程的实际 PID 进行初始化。
bpf_printk("BPF triggered from PID %d.\n", pid);
这是 BPF 等价物printf("Hello, world!\n")
!它将格式化的字符串发送到位于 的特殊文件中/sys/kernel/debug/tracing/trace_pipe
,您可以通过 cat 命令从控制台查看其内容(确保您sudo
在 root 下使用或运行):
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
<...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: BPF triggered from PID 3840345.
<...>-3840345 [010] d... 3220702.101265: bpf_trace_printk: BPF triggered from PID 3840345.
bpf_printk()
helper 和trace_pipe
file 不打算在生产中使用,但它对于调试 BPF 代码和深入了解 BPF 程序在做什么是必不可少的。由于目前还没有 BPF 调试器,bpf_printk()
因此通常是调试 BPF 代码中问题的最快和最方便的方法。
The user-space side
现在让我们看看如何从用户空间(minimum.c)将事物联系在一起,跳过一些非常明显的部分(无论如何请检查完整的源代码)。
#include "minimal.skel.h"
这包括minimal.bpf.c
. 它由 bpftool 在 Makefile 步骤之一中自动生成,反映了minimal.bpf.c
. 它还通过将编译后的 BPF 目标代码的内容嵌入头文件中来简化 BPF 代码部署逻辑,头文件包含在用户空间代码中。没有额外的文件要沿着你的应用程序二进制文件部署,只需包含头文件就可以了。
BPF skeleton纯粹是一种
libbpf
构造,内核对此一无所知。但对于 BPF 开发过程来说,这是一个巨大的生活质量改进,所以请考虑熟悉它。 有关 BPF skeleton的更多详细信息,请参阅博客文章。
src/.output/<app>.skel.h
成功make
调用后会生成 libbpf-bootstrap BPF skeleton。为了更好地了解它,以下是 的skeleton的高级概述minimal.bpf.c
:
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* THIS FILE IS AUTOGENERATED! */
#ifndef __MINIMAL_BPF_SKEL_H__
#define __MINIMAL_BPF_SKEL_H__
#include <stdlib.h>
#include <bpf/libbpf.h>
struct minimal_bpf {
struct bpf_object_skeleton *skeleton;
struct bpf_object *obj;
struct {
struct bpf_map *bss;
} maps;
struct {
struct bpf_program *handle_tp;
} progs;
struct {
struct bpf_link *handle_tp;
} links;
struct minimal_bpf__bss {
int my_pid;
} *bss;
};
static inline void minimal_bpf__destroy(struct minimal_bpf *obj) { ... }
static inline struct minimal_bpf *minimal_bpf__open_opts(const struct bpf_object_open_opts *opts) { ... }
static inline struct minimal_bpf *minimal_bpf__open(void) { ... }
static inline int minimal_bpf__load(struct minimal_bpf *obj) { ... }
static inline struct minimal_bpf *minimal_bpf__open_and_load(void) { ... }
static inline int minimal_bpf__attach(struct minimal_bpf *obj) { ... }
static inline void minimal_bpf__detach(struct minimal_bpf *obj) { ... }
#endif /* __MINIMAL_BPF_SKEL_H__ */
它具有struct bpf_object *obj;
可以传递给 libbpf API 函数的 。它还具有maps
、progs
和links
“部分”,提供对 BPF 映射和 BPF 代码中定义的程序(例如,handle_tp
BPF 程序)的直接访问。这些引用可以直接传递给 libbpf API 以对 BPF 映射/程序/链接做一些额外的事情。Skeleton 还可以选择有bss
、data
和rodata
部分,允许从用户空间直接(不需要额外的系统调用)访问 BPF 全局变量。在这种情况下,我们的my_pid
BPF 变量对应于bss->my_pid
字段。
现在看看main()
我们的minimal
应用程序做了什么:
int main(int argc, char **argv)
{
struct minimal_bpf *skel;
int err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
libbpf_set_print()
为所有 libbpf 日志提供自定义回调。这非常有用,尤其是在活跃开发期间,因为它允许捕获有用的 libbpf 调试日志。默认情况下,如果出现问题,libbpf 将仅记录错误级别的消息。但是,调试日志有助于获得有关正在发生的事情的额外上下文并更快地调试问题。
如果您需要报告 libbpf 和/或基于 libbpf 的应用程序的一些问题(例如,通过向bpf@vger.kernel.org邮件列表发送电子邮件 ),请始终包括来自 libbpf 的完整调试日志。
在minimal
’s 的情况下,libbpf_print_fn()
只是将所有内容发送到标准输出。
/* Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything */
bump_memlock_rlimit();
这是一个有点令人困惑但必要的步骤,几乎任何现实的 BPF 应用程序都必须这样做。它突破了内核的内部每个用户内存限制,以允许 BPF 子系统为您的 BPF 程序、地图等分配必要的资源。这个限制很可能很快就会消失,但现在你必须以一种或另一种方式来RLIMIT_MEMLOCK
限制。setrlimit(RLIMIT_MEMLOCK, ...)
像minimal
代码一样通过来做,是最简单、最方便的方法。
/* Load and verify BPF application */
skel = minimal_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
现在,使用自动生成的 BPF 框架,准备 BPF 程序并将其加载到内核中,并让 BPF 验证程序对其进行检查。如果此步骤成功,则您的 BPF 代码是正确的,并且可以附加到您需要的任何 BPF 挂钩上。
/* ensure BPF program only handles write() syscalls from our process */
skel->bss->my_pid = getpid();
但首先,我们需要将我们的 PID 传达给 BPF 代码,以便它可以write()
从不相关的进程中过滤掉不相关的调用。这通过内存映射区域直接设置 my_pid
BPF 全局变量值。如上所述,这就是用户空间访问(读取和写入)BPF 全局变量的方式。
/* Attach tracepoint handler */
err = minimal_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started!\n");
我们终于可以将handle_tp
BPF 程序(现在正在内核中等待)附加到相应的内核跟踪点。这“激活”了 BPF 程序,内核将开始在内核上下文中执行我们自定义的 BPF 代码以响应每个write()
系统调用!
libbpf 能够通过查看其特殊
SEC()
注释来自动确定将 BPF 程序附加到的位置。这并不适用于所有 可能的 BPF 程序类型,但它适用于很多类型:tracepoints、kprobes 和相当多的其他类型。此外,libbpf 提供额外的 API 以编程方式执行附件。
for (;;) {
/* trigger our BPF program */
fprintf(stderr, ".");
sleep(1);
}
这里的无限循环确保handle_tp
BPF 程序在内核中保持连接,直到用户终止进程(例如,通过按Ctrl-C
)。此外,它将通过write()
调用定期(每秒一次)生成系统调用fprintf(stderr, ...)
调用。通过这种方式,可以“监视”内核的内部结构handle_tp
以及状态如何随时间变化。
cleanup:
minimal_bpf__destroy(skel);
return -err;
}
如果任何前面的步骤出错,minimal_bpf__destroy()
将清理所有资源(在内核和用户空间中)。确保您始终执行此操作是一种很好的做法,但即使您的应用程序在未清理的情况下崩溃,内核仍会清理资源。好吧,至少在大多数情况下。有一些 BPF 程序类型即使所有者用户空间进程死亡也会在内核中保持活动状态,因此如果需要,请务必检查。
这就是minimal
应用程序的全部内容。BPF skeleton的使用使这一切变得非常简单。
Makefile
现在我们查看了minimal
应用程序,我们有足够的上下文来查看Makefile 做了什么将所有内容编译成最终可执行文件。我将跳过一些必要的样板部分,而只关注基本要素。
INCLUDES := -I$(OUTPUT)
CFLAGS := -g -Wall
ARCH := $(shell uname -m | sed 's/x86_64/x86/')
这里我们定义了一些编译过程中使用的额外参数。默认情况下,所有中间文件都会写在src/.output/
子目录下,所以这个目录被添加到C编译器的包含路径中,以查找BPFskeleton和libbpf头文件。所有用户空间文件都使用调试信息 ( -g
)进行编译,并且没有进行任何优化以使其更易于调试。ARCH
捕获主机操作系统架构,稍后将其传递到 BPF 代码编译步骤,以便与低级跟踪助手宏(在 libbpf 中bpf_tracing.h
)一起使用。
APPS = minimal bootstrap
这是可用应用程序的列表。如果您复制/粘贴minimal
或 bootstrap
创建自己的副本,只需在此处添加应用程序的名称即可构建。每个应用程序都定义了相应的 make 目标,因此您可以使用以下命令构建相关文件:
$ make minimal
整个构建过程分几个步骤进行。首先,libbpf 构建为静态库,其 API 头文件安装到.output/
:
# Build libbpf
$(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf
$(call msg,LIB,$@)
$(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \
OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \
INCLUDEDIR= LIBDIR= UAPIDIR= \
install
如果您想针对系统范围的libbpf
共享库进行构建,则可以删除此步骤并相应地调整编译规则。
下一步将 BPF C 代码 ( *.bpf.c
)构建到已编译的目标文件中:
# Build BPF code
$(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT)
$(call msg,BPF,$@)
$(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) -c $(filter %.c,$^) -o $@
$(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info
我们使用 Clang 来做到这一点。-g
必须让 Clang 发出 BTF 信息。 -O2
也是 BPF 编译所必需的。-D__TARGET_ARCH_$(ARCH)
为bpf_tracing.h
处理低级宏的头定义必要的struct pt_regs
宏。如果您不处理 kprobes 和 struct pt_regs
. 最后,我们从生成的.o
文件中去除 DWARF 信息,因为它从未使用过,而且大多只是 Clang 的编译工件。
BTF 是 BPF 功能所需的唯一信息,并且在剥离期间保留。减小
.bpf.o
文件的大小很重要,因为它将通过 BPF 框架嵌入到最终的应用程序二进制文件中,因此不需要用不需要的 DWARF 数据增加其大小。
现在我们已经.bpf.o
生成了文件,bpftool
用于生成相应的 BPF skeleton头 ( .skel.h
) 和bpftool gen skeleton
命令:
# Generate BPF skeletons
$(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT)
$(call msg,GEN-SKEL,$@)
$(Q)$(BPFTOOL) gen skeleton $< > $@
有了这个,我们确保每当 BPF 框架更新时,应用程序的用户空间部分也会重建,因为它们需要在编译期间嵌入 BPF 框架。否则,用户空间.c
→的编译.o
非常简单:
# Build user-space code
$(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h
$(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT)
$(call msg,CC,$@)
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
最后,仅使用用户空间.o
文件(与libbpf.a
静态库一起)生成最终的二进制文件。-lelf
并且-lz
是 libbpf 的依赖项,需要明确提供给编译器:
# Build application binary
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
$(call msg,BINARY,$@)
$(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@
就是这样,在运行完这几个步骤之后,您将最终得到一个小的用户空间二进制文件,该二进制文件通过 BPF 框架嵌入已编译的 BPF 代码,并在其中静态链接 libbpf,因此不依赖于系统范围的libbpf
可用性。结果是一个小 (200KB)、快速、独立的二进制文件,就像 Brendan Gregg 所问的那样。
Bootstrap app
现在我们已经介绍了minimal
app 以及如何在 中完成编译Makefile
,我们将通过bootstrap
app. bootstrap
是我在现代 BPF Linux 环境中编写生产就绪 BPF 应用程序的方式。它依赖于 BPF CO-RE(在此处阅读原因 )并需要使用构建的 Linux 内核CONFIG_DEBUG_INFO_BTF=y
(请参阅 此处)。
bootstrap
跟踪exec()
系统调用(使用SEC("tp/sched/sched_process_exec") handle_exit
BPF 程序),大致对应于新进程的产生(fork()
为了简单起见,忽略部分)。此外,它跟踪 exit()
s(使用SEC("tp/sched/sched_process_exit") handle_exit
BPF 程序)以了解每个进程何时退出。这两个 BPF 程序一起工作,允许捕获有关任何新进程的有趣信息,例如二进制文件名,以及测量进程的生命周期并在进程终止时收集有趣的统计信息,例如退出代码或消耗的资源量,等等。我发现这是深入内核内部结构并观察事物在幕后真正工作的一个很好的起点。
bootstrap
还使用argp API (libc 的一部分)进行命令行参数解析。请查看 “Step-by-Step into Argp”教程 ,了解有关argp
使用方法的精彩介绍。这是解析可选的最小进程生存期持续时间的方式(请参阅min_duration_ns
下面的只读变量;用于sudo ./bootstrap -d 100
仅显示存在至少 100 毫秒的进程),以及详细模式标志 (try sudo ./bootstrap -v
),启用 libbpf
调试日志。
Includes: vmlinux.h, libbpf and app headers
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"
这与minimal.bpf.c
我们现在使用的vmlinux.h
头文件不同,头文件将 Linux 内核中的所有类型都包含在一个文件中。它是 用 libbpf-bootstrap预先生成的,但也可以生成自定义的bpftool
(参见gen_vmlinux_h.sh)。
所有类型
vmlinux.h
都带有额外的__attribute__((preserve_access_index))
应用,这使得 Clang 生成 BPF CO-RE 重定位,允许 libbpf 使您的 BPF 代码适应主机内核的特定内存布局,即使它与vmlinux.h
最初生成的内存布局不同。这是构建便携式预编译 BPF 应用程序的一个关键方面,它不需要将整个 Clang/LLVM 工具链与其一起部署到目标系统。另一种 方法是在运行时编译 BPF 代码的BCC方式,它有很多缺点。
请记住,vmlinux.h
它不能与其他系统范围的内核头文件结合使用,因为您将不可避免地遇到类型重新定义和冲突。因此,请坚持只使用vmlinux.h
libbpf 提供的标头和应用程序的自定义标头,以避免不必要的麻烦。
此外,bpf_helpers.h
我们还使用了一些额外的 libbpf 提供的标头 bpf_tracing.h
和bpf_core_read.h
,它们为编写基于 BPF CO-RE 的跟踪 BPF 应用程序提供了一些额外的宏。
最后,bootstrap.h
包含通用类型定义,在 BPF 和bootstrap
应用程序的用户空间代码之间共享(对于 BPF ringbuf,见下文)。
BPF maps
bootstrap
演示了 BPF 映射的使用,这是抽象数据容器的 BPF 概念。许多不同的东西都被建模为 BPF 映射:从简单的数组和哈希映射到每个套接字和每个任务的本地存储、BPF 性能和环形缓冲区,甚至一些更奇特的用途。重要的是,大多数 BPF 映射允许通过某个键查找、更新和删除其元素。一些 BPF 映射允许额外的(或替代的)操作,比如 BPF ring buffer,它允许将数据入队,但永远不会从 BPF 端删除它。BPF 映射是在(可能很多)BPF 程序和用户空间之间共享状态的手段。另一个(对于存储简单的纯数据更高效和方便)是 BPF 全局变量(在幕后仍然使用 BPF 映射)。
在bootstrap
的情况下,我们定义了最大大小为 8192 个条目exec_start
的类型 BPF_MAP_TYPE_HASH
(哈希映射)的BPF 映射,键为pid_t
类型,值为 64 位无符号整数,存储进程执行的纳秒粒度时间戳事件。这就是所谓的 BTF 定义的地图。 SEC(".maps")
注释是强制性的,让 libbpf 知道它需要在内核中创建相应的 BPF 映射并在 BPF 代码中正确连接所有内容:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");
在此类哈希图中添加/更新条目很简单:
pid_t pid;
u64 ts;
/* remember time exec() was executed for this PID */
pid = bpf_get_current_pid_tgid() >> 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
bpf_map_update_elem()
BPF helper 获取指向映射本身的指针、键和值指针,以及额外的标志,在这种情况下 ( BPF_ANY
) 告诉要么添加新键,要么更新现有键。
注意第二个 BPF 程序 ( handle_exit
)如何从同一个 BPF 映射中查找元素并随后将其删除。这显示了exec_start
地图是如何在 两个 BPF 程序之间共享的:
pid_t pid;
u64 *start_ts;
...
start_ts = bpf_map_lookup_elem(&exec_start, &pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
...
bpf_map_delete_elem(&exec_start, &pid);
Read-only BPF configuration variables
bootstrap
,而不是minimal
,使用只读全局变量:
const volatile unsigned long long min_duration_ns = 0;
const volatile
部分很重要,它将变量标记为 BPF 代码和用户空间代码的只读。作为交换,它min_duration_ns
在 BPF 程序验证期间让 BPF 验证者知道变量的具体值 。这(由于更详细的知识)允许 BPF 验证器修剪死代码,如果只读值可证明省略了一些代码路径。此属性通常适用于一些更高级的用例,例如处理各种兼容性检查和额外配置。
volatile
有必要确保 Clang 不会完全优化掉变量,忽略用户空间提供的值。没有它,Clang 可以随意假设 0 并完全删除变量,这根本不是我们想要的。
从用户空间部分(在bootstrap.c 中)来看,初始化此类只读全局变量略有不同。它们需要在 BPF skeleton加载到内核之前设置。因此,bootstrap_bpf__open_and_load()
我们不需要使用单步,我们需要首先单独bootstrap_bpf__open()
的skeleton,设置只读变量值,然后bootstrap_bpf__load()
skeleton进入内核:
/* Load and verify BPF application */
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* Parameterize BPF code with minimum duration parameter */
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
/* Load & verify BPF programs */
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
请注意,这样的只读变量的一部分,rodata
在skeleton(不节data
或bss
)skel->rodata->min_duration_ns
。加载 BPF 框架后,用户空间代码只能读取只读变量的值。BPF 代码也只能读取此类变量。如果 BPF 验证器检测到写入此类变量的尝试,它将 拒绝BPF 程序。
BPF ring buffer
bootstrap
大量使用 BPF 环形缓冲区映射来准备数据并将数据发送回用户空间。它使用bpf_ringbuf_reserve()
/bpf_ringbuf_submit()
组合以获得最佳可用性和性能。请查看 BPF ring buffer post 以获得更全面的覆盖。该帖子详细介绍了非常相似的功能,查看了单独的 bpf-ringbuf-examples 存储库中的示例 。如果您选择这样做,它还应该让您对如何使用 BPF perf 缓冲区有一个很好的了解。
BPF CO-RE
BPF CO-RE(一次编译 - 到处运行)是一个相当大的话题,在专门的博客文章中单独介绍,请务必查看。这bootstrap.bpf.c
是使用 BPF CO-RE 功能从内核读取数据 的一个示例 struct task_struct
:
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
在非 BPF 世界中,它会被写成 just e->ppid = task->real_parent->tgid;
,但 BPF 验证器需要额外的努力,因为有读取任意内核内存的风险。BPF_CORE_READ()
以简洁的方式处理这个问题,并在此过程中记录必要的 BPF CO-RE 重定位,允许 libbpf 将所有字段偏移量调整为主机内核的特定内存布局。 有关更多示例,请参阅此帖子。
Conclusion
这应该是为了广泛覆盖libbpf-bootstrap
和各种 BPF/libbpf 方面。希望libbpf-bootstrap
这能让您克服设置一切以开始 BPF 开发的最初障碍,而是允许将更多时间花在 BPF 本身并修补内核可观察性、跟踪等方面。毕竟,这是使用 BPF 最令人兴奋的部分(至少对我而言)。
对于经验丰富的 BPF 开发人员,它应该展示了一种使用现代 BPF 可用性助推器(如 BPF skeleton、BPF ringbuf、BPF CO-RE)设置一切的方法(以防万一你没有密切关注 BPF 开发)。
因此,请检查Github 存储库 并试一试。带有错误修复和改进以及任何建议的 PR 总是受欢迎的。玩得开心 BPF!