使用libbpf-bootstrap构建BPF应用程序

蒋弘致
2023-12-01

目录

为什么选择libbpf-bootstrap?

先决条件

Libbpf引导概述

最小的应用

BPF方面

用户空间端

生成文件

引导程序

包括:vmlinux.h,libbpf和应用程序头

BPF地图

只读BPF配置变量

BPF环形缓冲区

BPF CO-RE

结论


 

libbpf-bootstrap脚手架可让您轻松快捷地开始使用自己的BPF应用程序,该脚手架将处理所有寻常的设置步骤,并使您能够 直接体验BPF的乐趣并最大程度地减少必要的样板。我们将看看libbpf-bootstrap提供了什么以及如何将所有东西捆绑在一起。

为什么选择libbpf-bootstrap?

BPF是一种了不起的内核技术,它使任何人都可以在没有丰富内核开发经验且无需花费大量时间为内核开发做好准备的情况下,就如何选择内核功能进行选择。BPF还消除了这样做时使操作系统崩溃的风险。一旦您掌握了BPF,您将获得很多乐趣和力量。

但是开始使用BPF仍然可能在很大程度上令人感到恐惧,因为即使为一个简单的类似“ Hello,World”的BPF应用程序设置构建工作流也需要一系列步骤,这些步骤可能会使新的BPF开发人员感到沮丧和恐惧。尽管实际上并没有那么复杂,但是了解必要的步骤是(不必要)困难的部分,尽管对BPF充满了兴趣和希望,但可能会使许多人什至无法尝试。

libbpf-bootstrap是一个脚手架游乐场,它为初学者设置了尽可能多的内容,使他们可以直接投入编写BPF程序并进行修补,而不会对初始设置造成不必要的麻烦。它考虑了BPF社区在过去几年中开发的最佳实践,并提供了现代便捷的工作流程,可以说是迄今为止最佳的BPF用户体验。libbpf-bootstrap依赖libbpf 并使用简单的Makefile。对于需要更高级设置的用户,这应该是一个很好的起点。至少,如果不能直接使用Makefile,将逻辑转移到需要使用的任何构建系统就足够简单了。

libbpf-bootstrap当前有两个演示BPF应用程序可用:minimal 和bootstrapminimal正是这样–编译,加载和运行等效于BPF的简单BPF的最小BPF应用程序printf("Hello, World!")。作为最起码的版本,它也没有对Linux内核的最新性提出任何要求,并且可以在相当老的内核版本上正常运行。

minimal非常适合用于快速实验和在本地进行尝试,但是它的设置并未反映出可在多种内核之间部署的基于生产的,基于BPF的应用程序的设置。bootstrap就是这样一个例子。bootstrap该演示演示了一种构建最小但功能齐全且可移植的 BPF应用程序的实际方法。为此,它确实依赖于BPF CO-RE 和内核BTF支持,因此请确保您的Linux内核是使用CONFIG_DEBUG_INFO_BTF=yKconfig构建的。请参阅libbpf README 以获取已经为您设置了所有内容的Linux发行版列表。如果您想最大程度地减少构建自定义内核的麻烦,请坚持使用任何主要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。不过,对于大多数BPF功能,Clang 10或更高版本应该可以正常工作,但是某些更高级的BPF CO-RE功能可能需要Clang 11甚至12(例如,对于某些较新的和更高级的CO-RE内置重载)。

libbpf-bootstrap与libbpf(作为Git子模块)和bpftool(仅用于x86-64体系结构)捆绑在一起,以避免依赖Linux发行版中可用的任何特定(可能已过时)的版本。您的系统还应该安装zliblibz-devzlib-devel包装)和libelf (libelf-develfutils-libelf-devel包装)。这些是libbpf正确编译和运行它所必需的依赖项。

这不是BPF技术本身的入门知识,因此假定您对BPF程序,BPF映射,BPF挂钩(连接点)等基本概念有所了解。如果您需要复习BPF基础知识,那么这些 资源 应该是一个很好的起点。

在本文的其余部分,我将带您了解libbpf-bootstrap的结构 ,其Makefileminimal以及bootstrap示例和示例。我们将研究libbpf约定和结构化将BPF C代码与libbpf一起用作BPF程序加载器,以及如何使用libbpf API从用户空间与BPF程序进行交互。

Libbpf引导概述

这是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框架。与libbpf类似,它捆绑在一起以避免依赖于系统范围内的bpftool可用性及其版本是否足够最新。

此外,bpftool可用于生成vmlinux.h具有所有Linux内核类型定义的自己的标头。您可能不需要这样做,因为libbpf-bootstrap已经 在子目录中提供了预生成的 vmlinux.hsrc/。它基于Linux 5.8的默认内核配置,并启用了许多额外的BPF相关功能。这意味着它应该已经具有许多常用的内核类型和常量。由于BPF CO-REvmlinux.h不必完全匹配您的内核配置和版本。但是,如果您确实需要生成自定义vmlinux.h,请随时检查 tools/gen_vmlinux_h.sh 脚本以了解如何完成。

超越自我解释LICENSEREADME.md其余libbpf-bootstrap 被包含在一个src/子目录。

Makefile 定义了必要的构建规则,以编译所有提供的(和您自定义的)BPF应用程序。它遵循一个简单的文件命名约定:

  • <app>.bpf.c 文件是BPF C代码,其中包含要在内核上下文中执行的逻辑;
  • <app>.c 是用户空间C代码,它在应用程序的整个生命周期中加载BPF代码并与其交互;
  • 可选的 <app>.h是具有常见类型定义的头文件,并且由应用程序的BPF和用户空间代码共享。

因此,minimal.cminimal.bpf.c形成minimalBPF演示应用程序。和 bootstrap.cbootstrap.bpf.cbootstrap.hbootstrapBPF应用程序。简单的。

最小的应用

minimal是一个很好的例子。将其视为尝试BPF事情的极简主义游乐场。它不使用BPF CO-RE,因此您可以使用较旧的内核,而只需将系统内核标头包含在内核类型定义中即可。这不是构建可用于生产环境的应用程序和工具的最佳方法,但足以用于本地实验。

BPF方面

这里的BPF端代码(minimal.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应用程序使用的宏,常量和BPF帮助程序定义bpf_helpers.h提供libbpf并包含它们。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()任何用户空间应用程序调用syscall时都会调用程序。

同一BPF C代码文件中可能定义了许多BPF程序。它们可以具有不同的类型(即SEC()注释)。例如,您可以拥有几个不同的BPF程序,每个程序都用于不同的跟踪点或其他内核事件(例如,正在处理的网络数据包等)。您还可以使用相同的SEC()属性定义多个BPF程序:libbpf可以很好地处理。在同一BPF C代码文件中定义的所有BPF程序共享所有全局状态(如my_pid变量,也可以使用任何BPF映射(如果使用))。这经常用于协调很少的协作BPF程序。

现在让我们看一下handle_tpBPF程序在做什么:

	int pid = bpf_get_current_pid_tgid() >> 32;

	if (pid != my_pid)
		return 0;

这部分获取以bpf_get_current_pid_tgid()返回值的高32位编码的PID(或内部内核术语中的“ TGID”)。然后,它检查触发write()syscall的minimal进程是否是我们的进程。这在繁忙的系统上非常重要,因为很可能许多不相关的进程都将发出write(),这使得按自己的条件来尝试自己的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,您可以从控制台查看其内容(确保使用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()帮助程序和trace_pipe文件不打算在生产中使用,但是它对于调试BPF代码和洞悉BPF程序的工作必不可少。由于还没有BPF调试器,bpf_printk()因此通常是调试BPF代码中问题的最快,最方便的方法。

对于minimal应用程序的BPF端来说就是这样。随时将任何额外的代码添加到handle_tp()BPF程序的正文中,并根据需要进行扩展。

用户空间端

现在,让我们看一下如何将内容与用户空间(minimal.c)捆绑在一起,并跳过一些非常明显的部分(无论如何请检查完整的源代码)。

#include "minimal.skel.h"

这包括中的BPF代码的BPF框架minimal.bpf.c。它是由bpftool在Makefile步骤之一中自动生成的,反映了的高级结构minimal.bpf.c。它还通过将已编译的BPF目标代码的内容嵌入到头文件中来简化BPF代码部署后勤工作,该头文件从用户空间代码中包含在内。无需在应用程序二进制文件中部署额外的文件,只需添加标头就可以了。

BPF框架纯粹是一个libbpf构造,内核对此一无所知。但这对于BPF开发过程来说是极大的生活质量改善,所以请考虑熟悉它。 有关BPF框架的更多详细信息,请参见博客文章

src/.output/<app>.skel.h 成功make调用后,将生成libbpf-bootstrap BPF框架。为了更好地了解它,下面是对以下方面的简要概述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函数的功能。它还具有mapsprogslinks“部分”,可以直接访问BPF映射和BPF代码中定义的程序(例如handle_tpBPF程序)。这些引用可以直接传递给libbpf API,以对BPF映射/程序/链接进行更多操作。骨架还可以选择具有bssdatarodata部分,以允许直接(无需额外的系统调用)从用户空间访问BPF全局变量。在这种情况下,我们的my_pidBPF变量对应于该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这种情况下,libbpf_print_fn()只需将所有内容发送到stdout。

	/* 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_pidBPF全局变量值。如上所述,这是用户空间可以访问(读取和写入)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_tpBPF程序附加到相应的内核跟踪点,该程序现在已经在内核中等待了。这将“激活” BPF程序,并且内核将响应每次write()系统调用而开始在内核上下文中执行我们的自定义BPF代码!

libbpf能够通过查看其特殊SEC()注释来自动确定将BPF程序附加到何处。这不适用于所有 可能的BPF程序类型,但不适用于许多类型:跟踪点,kprobes和许多其他类型。另外,libbpf提供了额外的API以编程方式执行附件。

	for (;;) {
		/* trigger our BPF program */
		fprintf(stderr, ".");
		sleep(1);
	}

这里的无穷循环确保handle_tpBPF程序在用户终止进程之前(例如,通过按Ctrl-C)一直保持附加在内核中。同样,它将通过write()调用定期(每秒)生成syscall调用fprintf(stderr, ...)。这样,就有可能“监视”内核内部handle_tp以及状态随时间的变化。

cleanup:
	minimal_bpf__destroy(skel);
	return -err;
}

如果上述任何步骤出错,minimal_bpf__destroy()将清除所有资源(包括内核和用户空间中的资源)。确保始终执行此操作是一个好习惯,但是即使您的应用程序崩溃而没有清理,内核仍会清理资源。好吧,至少在大多数情况下。即使所有者用户空间进程死了,有些BPF程序类型仍将在内核中保持活动状态,因此请确保在必要时进行检查。

这对于minimal应用程序来说就差不多了。BPF框架的使用使这一切变得非常简单。

生成文件

既然我们已经查看了该minimal应用程序,我们就有足够的上下文来查看Makefile的 工作,以将所有内容编译为最终可执行文件。我将跳过一些必要的样板部分,而只专注于要点。

INCLUDES := -I$(OUTPUT)
CFLAGS := -g -Wall
ARCH := $(shell uname -m | sed 's/x86_64/x86/')

在这里,我们定义了一些在编译过程中使用的额外参数。默认情况下,所有中间文件都将写入src/.output/子目录下,因此此目录将添加到C编译器的包含路径中,以查找BPF框架和libbpf标头。所有用户空间文件都使用调试信息(-g)进行编译,并且没有进行任何优化以使其更易于调试。ARCH捕获主机OS体系结构,该体系结构随后将传递给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信息是强制性的。 -O2BPF编译也是必需的。-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用于.skel.h通过以下bpftool gen skeleton 命令生成相应的BPF框架标头():

# 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要求的那样

引导程序

既然我们已经介绍了minimal应用程序以及如何在中完成编译Makefile,我们将通过bootstrap应用程序演示一些额外的BPF功能。 bootstrap这就是我如何在现代BPF Linux环境中编写可用于生产环境的BPF应用程序。它依赖于BPF CO-RE(请在此处阅读原因 ),并且需要使用Linux内核构建CONFIG_DEBUG_INFO_BTF=y(请参见 此处)。

bootstrap跟踪exec()系统调用(使用SEC("tp/sched/sched_process_exec") handle_exitBPF程序),大致对应于新进程的产生(fork()为简单起见,忽略该部分)。另外,它跟踪 exit()s(使用SEC("tp/sched/sched_process_exit") handle_exitBPF程序)以了解每个进程何时退出。这两个BPF程序一起工作,可以捕获有关任何新进程的有趣信息,例如二进制文件的文件名,还可以测量该进程的生命周期,并在进程终止时收集有趣的统计信息,例如退出代码或消耗的资源数量,等。我发现这是一个深入了解内核内部结构并观察事物在幕后如何真正工作的好起点。

bootstrap还使用argp API (libc的一部分)进行命令行参数解析。请查看 “逐步导入Argp”教程 ,以获取argp用法的详细介绍。这是解析可选的最小进程生存期持续时间的方式(请参阅min_duration_ns下面的只读变量;用于sudo ./bootstrap -d 100仅显示存在至少100ms的进程)以及详细模式标志(尝试sudo ./bootstrap -v),以启用 libbpf调试日志。

包括:vmlinux.h,libbpf和应用程序头

这是BPF方面的包含部分(bootstrap.bpf.c):

#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预先生成的,但是也可以使用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它不能与其他系统级内核标头结合使用,因为您不可避免地会遇到类型重新定义和冲突。因此,请坚持使用just vmlinux.h,libbpf提供的标头以及应用程序的自定义标头,以避免不必要的麻烦。

除此以外,bpf_helpers.h我们还使用了一些额外的libbpf提供的标头 bpf_tracing.hbpf_core_read.h,它们提供了一些额外的宏来编写BPF基于CO-RE的跟踪BPF应用程序。

最后,bootstrap.h包含通用类型定义,这些通用类型定义在BPF和bootstrap应用程序的用户空间代码之间共享(有关BPF ringbuf,请参见下文)。

BPF地图

bootstrap演示了BPF映射的使用,BPF映射是抽象数据容器的BPF概念。许多不同的事物被建模为BPF映射:从简单的数组和哈希映射到每个套接字和每个任务的本地存储,BPF的perf和ring缓冲区,甚至还有一些其他用途。重要的是,大多数BPF映射允许通过某些键来查找,更新和删除其元素。一些BPF映射允许额外的(或替代的)操作,例如 BPF环形缓冲区,该操作可以使数据入队,但从不从BPF端删除它。BPF映射是在(可能很多)BPF程序用户空间之间共享状态的方法。另一个是BPF全局变量(在后台仍在使用BPF映射)(性能更高且更方便存储简单的简单数据)。

bootstrap的情况下,我们定义exec_start类型 为BPF的映射BPF_MAP_TYPE_HASH(哈希映射),最大大小为8192个条目,键为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助手使用指向映射本身的指针,键和值指针以及额外的标志,在这种情况下(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);

只读BPF配置变量

bootstrap与相对minimal,使用了只读全局变量:

const volatile unsigned long long min_duration_ns = 0;

const volatile部分很重要,它将变量标记为BPF代码和用户空间代码为只读。作为交换,它使min_duration_nsBPF验证程序在BPF程序验证期间将变量的特定值 告知BPF验证程序。如果可以证明只读值省略了某些代码路径,则这(由于更详细的知识)允许BPF验证者修剪死代码。对于某些更高级的用例,例如处理各种兼容性检查和额外的配置,通常需要此属性。

volatile确保Clang不会完全忽略变量,忽略用户空间提供的值是必要的。没有它,Clang可以随意假设0并完全删除变量,这根本不是我们想要的。

从用户空间部分(在bootstrap.c中),初始化此类只读全局变量略有不同。需要在BPF骨架加载到内核之前进行设置。因此,除了使用单一步骤之外bootstrap_bpf__open_and_load(),我们还需要先分别bootstrap_bpf__open()框架,设置只读变量值,然后再将bootstrap_bpf__load()框架放入内核:

	/* 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框架(databss)中section的一部分skel->rodata->min_duration_ns。加载BPF框架后,用户空间代码只能读取只读变量的值。BPF代码也只能读取此类变量。如果BPF验证程序检测到尝试写入该变量的行为,它将 拒绝BPF程序。

BPF环形缓冲区

bootstrap正在大量使用BPF环形缓冲区映射来准备数据并将其发送回用户空间。它使用bpf_ringbuf_reserve()/bpf_ringbuf_submit() 组合来获得最佳可用性和性能。请检查BPF环形缓冲区 更彻底覆盖。该帖子详细介绍了非常相似的功能,在单独的 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可以将所有字段偏移量调整为主机内核的特定内存布局。请参阅此帖子 以获取更多示例。

结论

对于广泛的libbpf-bootstrapBPF / libbpf方面以及广泛的方面,应该这样做。希望,libbpf-bootstrap这将使您克服最初的障碍,即开始进行BPF开发所需的一切设置,而将允许您将更多的时间花在BPF本身上,并对内核的可观察性,跟踪功能进行改进。毕竟,那是使用BPF最令人兴奋的部分(至少对我而言)。

对于经验丰富的BPF开发人员,它应该演示了一种使用现代BPF可用性增强程序(如BPF框架,BPF ringbuf,BPF CO-RE)进行设置的方法(以防万一您没有密切关注BPF开发)。

因此,请检查Github仓库 并尝试一下。始终欢迎带有错误修复和改进以及任何建议的PR。与BPF一起玩吧!

 

 

 类似资料: