使用 eBPF 扩展内核

扩展型柏克莱封包过滤器 (eBPF) 是一个内核中的虚拟机,可运行用户提供的 eBPF 程序来扩展内核功能。这些程序可以挂接到内核中的探测点或事件,并用于收集有用的内核统计信息、监控和调试。程序使用 bpf(2) 系统调用加载到内核中,并作为 eBPF 机器指令的二进制 blob 由用户提供。Android 构建系统支持使用本文所述的简单 build 文件语法将 C 程序编译为 eBPF 程序。

如需详细了解 eBPF 内部构件和架构,请参阅 Brendan Gregg 的 eBPF 页面

Android 包含一个 eBPF 加载器和库,它可在 Android 启动时加载 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 套接字,程序就会运行。

您可以在加载器源代码中找到更多类型。

例如,以下 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,属于系统映像的一部分。该库向用户提供了执行以下操作所需的低级别 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 内存性能分析。