文章目录
前言
本文参考了相关博客:Building BPF applications with libbpf-bootstrap
选择libbpf+BPF CO-RE的理由
libbpf 是一个比BCC更新的 BPF 开发库,也是最新的 BPF 开发推荐方式,以下是从BCC转向libbpf+BPF CO-RE的理由:
BCC 依靠运行时间汇编,将整个大型 LLVM/Clang 库带入并嵌入其中。这有许多后果,所有这些都不太理想:
- 编译过程中资源利用量大(内存和 CPU),可能会中断繁忙服务器上的主要工作流;
- 依赖于内核头包,该包必须安装在每个目标主机上。即便如此,如果您需要从内核中获取不通过public headers暴露的东西,您也需要手工将类型定义复制/粘贴到 BPF 代码中,以完成您的工作;
- 即使是微不足道的编译时间错误也只在运行时间(在您完全重建并重新启动用户空间应用程序后)检测到:这显著缩短了开发迭代时间(并增加了挫折感水平。。。)
文章BPF Portability and CO-RE 指出,为了提高BPF程序的便携性,即在不同内核版本上正常工作,而无需为每个特定内核重新编译的能力,社区提出了一个称为BPF CO-RE(Compile Once – Run Everywhere)的解决方案。
Libbpf+BPF CO-RE的理念是,BPF程序与任何"正常"用户空间程序没有太大区别:它们应该汇编成小型二进制文件,然后以紧凑的形式进行部署,以瞄准主机。Libbpf 扮演 BPF 程序装载机的角色,执行平凡的设置工作(重定位、加载和验证 BPF 程序、创建 BPF map、连接到 BPF 挂钩等),让开发人员只担心 BPF 程序的正确性和性能。这种方法将开销保持在最低水平,消除沉重的依赖关系,使整体开发人员体验更加愉快。
使用libbpf-bootstrap的理由
开始使用 BPF 在很大程度上仍然令人生畏,因为即使为简单的"Hello World"般的 BPF 应用程序设置构建工作流,也需要一系列步骤,对于新的 BPF 开发人员来说,这些步骤可能会令人沮丧和令人生畏。这并不复杂,但知道必要的步骤是一个(不必要的)困难的部分。
libbpf-bootstrap 就是这样一个 BPF 游乐场,它已经尽可能地为初学者配置好了环境,帮助他们可以直接步入到 BPF 程序的书写。它综合了 BPF 社区多年来的最佳实践,并且提供了一个现代化的、便捷的工作流。libbpf-bootstrap 依赖于 libbpf 并且使用了一个很简单的 Makefile。对于需要更高级设置的用户,它也是一个好的起点。即使这个 Makefile不会被直接使用到,也可以很轻易地迁移到别的构建系统上。
Libbpf-bootstrap结构
Libbpf-bootstrap的内容如下:
$ tree -d
.
├── examples
│ ├── c
│ │ ├── CMakeFiles
│ │ │ ├── 3.18.4
│ │ │ │ ├── CompilerIdC
│ │ │ │ │ └── tmp
│ │ │ │ └── CompilerIdCXX
│ │ │ │ └── tmp
│ │ │ ├── bootstrap.dir
│ │ │ ├── CMakeTmp
│ │ │ ├── fentry.dir
│ │ │ ├── kprobe.dir
│ │ │ ├── libbpf-build.dir
│ │ │ ├── libbpf.dir
│ │ │ ├── minimal.dir
│ │ │ └── uprobe.dir
│ │ └── libbpf
│ │ ├── bpf
│ │ ├── libbpf
│ │ │ └── staticobjs
│ │ ├── pkgconfig
│ │ ├── src
│ │ │ └── libbpf-stamp
│ │ └── tmp
│ └── rust
│ ├── tracecon
│ │ └── src
│ │ └── bpf
│ └── xdp
│ └── src
│ └── bpf
├── libbpf
│ ├── include
│ │ ├── asm
│ │ ├── linux
│ │ └── uapi
│ │ └── linux
│ ├── scripts
│ ├── src
│ │ ├── sharedobjs
│ │ └── staticobjs
│ └── travis-ci
│ ├── managers
│ └── vmtest
│ └── configs
│ ├── blacklist
│ └── whitelist
├── tools
│ └── cmake
└── vmlinux
51 directories
libbpf-bootstrap
捆绑libbpf作为子目录中的子模块,以避免取决于全系统的libbpf可用性和版本。这意味着,拉取时拉不了libbpf
这个文件下,需要你自己定制(再拉一次…)。
libbpf/tools/
包含二进制文件,用于构建BPF代码的BPF骨架。与 libbpf 类似,它被捆绑以避免取决于全系统的 bpftool
可用性及其版本是否足够最新。
此外,bpftool 可用于生成具有所有 Linux 内核类型定义的您自己的头文件vmlinux.h
,文章【踩坑】使用libbpfgo构建你的第一个eBPF项目就有类似操作。
但很可能你不需要这样做, 因为利布普夫引导已经在子目录中提供了预生成的vmlinux. h 。它基于 Linux 5.8 的默认内核配置,启用了一系列额外的 BPF 相关功能。这意味着它应该已经有很多常用的内核类型和常数。由于BPF CO-RE,不必完全匹配您的内核配置和版本。但是,如果您确实需要生成自定义内核,请随时查看工具/gen_vmlinux_h.sh
脚本,看看如何做到这一点。
Makefile定义了必要的生成规则,以编译所有提供的(和自定义的)BPF 应用程序。它遵循一个简单的文件命名惯例:
- <app>.bpf.c文件是包含在内核上下文中执行的逻辑的 BPF C 代码;
- <app>.c是用户空间 C 代码,它加载 BPF 代码并在应用程序的整个使用寿命内与它交互:
- 可选的是具有通用类型定义的标头文件,由应用的 BPF 和用户空间代码共享。<app>.h
Minimal:最小应用程序分析
minimal
是一个很好的尝试。把它当作一个极简主义的游乐场, 尝试 Bpf 的东西。它不使用BPF CO-RE,因此您可以使用较旧的内核,只需将系统内核头用于内核类型定义。这不是构建生产应用和工具的最佳方法,但对于本地实验来说已经足够了。
minimal
使用bpf_printk()
这一经典方法,将内容输出到/sys/kernel/debug/tracing/trace_pipe
,使用root权限可以进行查看。
运行minimal:
按照官方教程:
$ cd examples/c
$ make minimal
$ sudo ./minimal
$ 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.
minimal
是一个几乎仅有骨架的BPF程序,适合做新程序的实验。
代码分析:BPF side
以下是BPF side的应用程序代码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 helper 函数标志)所必需的。由最常用的宏、常数和 BPF 助手定义提供并包含这些定义,几乎每个现有 BPF 应用程序都使用这些定义。
LICENSE
变量定义了BPF代码的许可证。指定许可证是强制性的,由内核强制执行。某些 BPF 功能不可用于非 GPL 兼容代码。注意特殊注释。 (由 )将变量和功能放入指定部分。,沿着一些其他部分的名称,是惯例所决定的,所以一定要坚持下去。
接下来,我们看到使用令人兴奋的BPF功能:global变量。 做你所期望的:它定义了一个global变量,BPF代码可以读取和更新,就像任何用户空间C代码将做一个global变量。使用 BPF global变量来维护 BPF 计划的状态非常方便,而且性能也非常出色。此外,此类global变量可以从用户空间方面读取和编写。此功能可从 Linux 5.5 版本开始。它经常用于诸如配置具有额外设置的 BPF 应用程序、低开销统计信息等。它还可用于在内核 BPF 代码和用户空间控制代码之间来回传递数据。int my_pid = 0;
SEC("tp/syscalls/sys_enter_write") int handle_tp(void *ctx) { ... }
定义将加载到内核中的BPF程序。在特别命名的部分(使用宏)中,它表示为普通 C 函数。节名称定义了 BPF 程序 libbpf 应该创建的类型以及如何/在哪里可以将其附加到内核中。在这种情况下,我们定义了一个跟踪点 BPF 程序,每次从任何用户空间应用程序调用系统呼叫时,都会将其调用。
可能在同一 BPF C 代码文件中定义了许多 BPF 程序。它们可能具有不同的类型(即注释)。例如,您可以有几个不同的 BPF 程序,每个程序用于不同的跟踪点或其他一些内核事件(例如,正在处理的网络数据包等)。您还可以定义具有相同属性的多个 BPF 程序:将处理得很好。在同一 BPF C 代码文件中定义的所有 BPF 程序共享所有全球状态(如变量,但使用时还可以共享任何 BPF 地图)。这经常被用于协调很少的合作 BPF 程序。
接下来看handle_tp()
打算做什么:
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid !=