使用 eBPF 扩展内核

扩展 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 功能。

将程序附加到跟踪点

跟踪点程序在启动时自动加载。加载后,必须使用以下步骤激活跟踪点程序

  1. 调用 bpf_obj_get() 以从固定文件的位置获取程序 fd。有关更多信息,请参阅sysfs 中提供的文件
  2. 在 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 内存分析。