扩展 Berkeley 数据包过滤器 (eBPF) 是一种内核虚拟机,可运行用户提供的 eBPF 程序来扩展内核功能。这些程序可以挂钩到内核中的探针或事件,并用于收集有用的内核统计信息、进行监控和调试。程序使用 bpf(2)
系统调用加载到内核中,并由用户以 eBPF 机器指令的二进制 blob 形式提供。Android 构建系统支持使用本文档中描述的简单构建文件语法将 C 程序编译为 eBPF。
有关 eBPF 内部结构和架构的更多信息,请访问 Brendan Gregg 的 eBPF 页面。
Android 包含一个 eBPF 加载器和库,用于在启动时加载 eBPF 程序。
Android BPF 加载器
在 Android 启动期间,位于 /system/etc/bpf/
的所有 eBPF 程序都会被加载。这些程序是由 Android 构建系统从 C 程序构建的二进制对象,并且在 Android 源代码树中附带有 Android.bp
文件。构建系统将生成的对象存储在 /system/etc/bpf
中,这些对象将成为系统映像的一部分。
Android eBPF C 程序的格式
eBPF C 程序必须具有以下格式
#include <bpf_helpers.h>
/* Define one or more maps in the maps section, for example
* define a map of type array int -> uint32_t, with 10 entries
*/
DEFINE_BPF_MAP(name_of_my_map
, ARRAY, int, uint32_t, 10);
/* this also defines type-safe accessors:
* value * bpf_name_of_my_map_lookup_elem(&key);
* int bpf_name_of_my_map_update_elem(&key, &value, flags);
* int bpf_name_of_my_map_delete_elem(&key);
* as such it is heavily suggested to use lowercase *_map names.
* Also note that due to compiler deficiencies you cannot use a type
* of 'struct foo' but must instead use just 'foo'. As such structs
* must not be defined as 'struct foo {}' and must instead be
* 'typedef struct {} foo'.
*/
DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
<body-of-code
... read or write to MY_MAPNAME
... do other things
>
}
LICENSE("GPL"); // or other license
其中:
name_of_my_map
是您的映射变量的名称。此名称会告知 BPF 加载器要创建的映射类型以及使用的参数。此结构定义由包含的bpf_helpers.h
标头提供。PROGTYPE/PROGNAME
表示程序类型和程序名称。程序类型可以是下表列出的任何一种。当程序类型未列出时,程序没有严格的命名惯例;只需将名称告知附加程序的过程即可。PROGFUNC
是一个函数,编译后会放置在结果文件的某个部分中。
kprobe | 使用 kprobe 基础架构将 PROGFUNC 挂钩到内核指令。 PROGNAME 必须是正在进行 kprobe 的内核函数的名称。有关 kprobe 的更多信息,请参阅 kprobe 内核文档。 |
---|---|
tracepoint | 将 PROGFUNC 挂钩到跟踪点。 PROGNAME 的格式必须为 SUBSYSTEM/EVENT 。例如,用于将函数附加到调度程序上下文切换事件的跟踪点部分将是 SEC("tracepoint/sched/sched_switch") ,其中 sched 是跟踪子系统的名称,而 sched_switch 是跟踪事件的名称。有关跟踪点的更多信息,请查看 跟踪事件内核文档。 |
skfilter | 程序充当网络套接字过滤器。 |
schedcls | 程序充当网络流量分类器。 |
cgroupskb、cgroupsock | 只要 CGroup 中的进程创建 AF_INET 或 AF_INET6 套接字,程序就会运行。 |
其他类型可以在 Loader 源代码中找到。
例如,以下 myschedtp.c
程序添加了有关在特定 CPU 上运行的最新任务 PID 的信息。此程序通过创建映射并定义可以附加到 sched:sched_switch
跟踪事件的 tp_sched_switch
函数来实现其目标。有关更多信息,请参阅将程序附加到跟踪点。
#include <linux/bpf.h> #include <stdbool.h> #include <stdint.h> #include <bpf_helpers.h> DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024); struct switch_args { unsigned long long ignore; char prev_comm[16]; int prev_pid; int prev_prio; long long prev_state; char next_comm[16]; int next_pid; int next_prio; }; DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch) (struct switch_args *args) { int key; uint32_t val; key = bpf_get_smp_processor_id(); val = args->next_pid; bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY); return 1; // return 1 to avoid blocking simpleperf from receiving events } LICENSE("GPL");
当程序使用内核提供的 BPF 辅助函数时,LICENSE 宏用于验证程序是否与内核的许可协议兼容。以字符串形式指定程序的许可协议名称,例如 LICENSE("GPL")
或 LICENSE("Apache 2.0")
。
Android.bp 文件的格式
为了让 Android 构建系统构建 eBPF .c
程序,您必须在项目的 Android.bp
文件中创建一个条目。例如,要构建名为 bpf_test.c
的 eBPF C 程序,请在项目的 Android.bp
文件中创建以下条目
bpf { name: "bpf_test.o", srcs: ["bpf_test.c"], cflags: [ "-Wall", "-Werror", ], }
此条目编译 C 程序,从而生成对象 /system/etc/bpf/bpf_test.o
。启动时,Android 系统会自动将 bpf_test.o
程序加载到内核中。
sysfs 中提供的文件
在启动期间,Android 系统会自动从 /system/etc/bpf/
加载所有 eBPF 对象,创建程序需要的映射,并将加载的程序及其映射固定到 BPF 文件系统。然后,这些文件可用于与 eBPF 程序进行进一步交互或读取映射。本节介绍用于命名这些文件及其在 sysfs 中的位置的惯例。
将创建并固定以下文件
对于加载的任何程序,假设
PROGNAME
是程序的名称,而FILENAME
是 eBPF C 文件的名称,则 Android 加载器会在/sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME
创建并固定每个程序。例如,对于之前
myschedtp.c
中的sched_switch
跟踪点示例,程序文件将被创建并固定到/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch
。对于创建的任何映射,假设
MAPNAME
是映射的名称,而FILENAME
是 eBPF C 文件的名称,则 Android 加载器会在/sys/fs/bpf/map_FILENAME_MAPNAME
创建并固定每个映射。例如,对于之前
myschedtp.c
中的sched_switch
跟踪点示例,映射文件将被创建并固定到/sys/fs/bpf/map_myschedtp_cpu_pid_map
。Android BPF 库中的
bpf_obj_get()
从固定的/sys/fs/bpf
文件返回文件描述符。此文件描述符可用于进一步操作,例如读取映射或将程序附加到跟踪点。
Android BPF 库
Android BPF 库名为 libbpf_android.so
,是系统映像的一部分。此库为用户提供创建和读取映射、创建探针、跟踪点和 perf 缓冲区所需的低级别 eBPF 功能。
将程序附加到跟踪点
跟踪点程序在启动时自动加载。加载后,必须使用以下步骤激活跟踪点程序
- 调用
bpf_obj_get()
以从固定文件的位置获取程序fd
。有关更多信息,请参阅sysfs 中提供的文件。 - 在 BPF 库中调用
bpf_attach_tracepoint()
,将程序fd
和跟踪点名称传递给它。
以下代码示例展示了如何附加在之前的 myschedtp.c
源文件中定义的 sched_switch
跟踪点(未显示错误检查)
char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch"; char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid"; // Attach tracepoint and wait for 4 seconds int mProgFd = bpf_obj_get(tp_prog_path); int mMapFd = bpf_obj_get(tp_map_path); int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch"); sleep(4); // Read the map to find the last PID that ran on CPU 0 android::bpf::BpfMap<int, int> myMap(mMapFd); printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));
从映射中读取
BPF 映射支持任意复杂的键和值结构或类型。Android BPF 库包含一个 android::BpfMap
类,该类使用 C++ 模板来基于所讨论映射的键和值类型实例化 BpfMap
。之前的代码示例演示了如何将 BpfMap
与整数类型的键和值一起使用。整数也可以是任意结构。
因此,模板化的 BpfMap
类允许您定义适合特定映射的自定义 BpfMap
对象。然后可以使用自定义生成且类型感知的函数来访问映射,从而使代码更简洁。
有关 BpfMap
的更多信息,请参阅 Android 源代码。
调试问题
在启动期间,会记录多条与 BPF 加载相关的消息。如果加载过程因任何原因失败,则会在 logcat 中提供详细的日志消息。通过 bpf
过滤 logcat 日志会打印所有消息以及加载期间的任何详细错误,例如 eBPF 验证程序错误。
Android 中 eBPF 的示例
AOSP 中的以下程序提供了使用 eBPF 的其他示例
netd
eBPF C 程序由 Android 中的网络守护程序 (netd) 用于各种目的,例如套接字过滤和统计信息收集。要了解此程序的使用方式,请查看 eBPF 流量监控器 源代码。time_in_state
eBPF C 程序计算 Android 应用在不同 CPU 频率下花费的时间量,该时间量用于计算功耗。在 Android 12 中,
gpu_mem
eBPF C 程序跟踪每个进程和整个系统的 GPU 内存总用量。此程序用于 GPU 内存分析。