使用 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 will also define 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 内存性能分析。