Android 包含一个 eBPF 加载器和库,它可在 Android 启动时加载 eBPF 程序以扩展内核功能。这可用于从内核收集统计信息,进行监控或调试。

eBPF 简介

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

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

Android BPF 加载器

在 Android 启动期间,系统会加载位于 /system/etc/bpf/ 的所有 eBPF 程序。这些程序是 Android 构建系统根据 C 程序和 Android 源代码树中的  Android.bp 文件构建而成的二进制对象。构建系统将生成的对象存储在  /system/etc/bpf ,这些对象将成为系统映像的一部分。

Android eBPF C 程序的格式

加载到 Android 设备上的 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'.
*/


SEC
("PROGTYPE/PROGNAME")
int PROGFUNC(..args..) {
<body-of-code
... read or write to MY_MAPNAME
... do other things
>
}

char _license[] SEC("license") = "GPL"; // or other license

其中, name_of_my_map 是映射变量的名称。它可告知 BPF 加载器要使用哪些参数来创建哪种类型的映射。此结构体定义由 C 程序包含的  bpf_helpers.h 头文件提供。运行以上代码会创建包含 10 个条目的数组映射。

接下来,该程序会定义 PROGFUNC 函数。编译时,系统会将此函数放在一个区段中。该区段的名称必须采用  PROGTYPE/PROGNAME 格式。 PROGTYPE 可以是以下任意一项。您可以在加载器源代码中找到更多类型。

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 套接字,程序就会运行。

例如,下面是一个完整的 C 程序,它创建了一个映射并定义了一个 tp_sched_switch 函数,该函数可以附加到  sched:sched_switch trace 事件(要了解如何附加,请参阅此部分)。该程序添加了与曾在特定 CPU 上运行的最新任务 PID 相关的信息。将其命名为  myschedtp.c 。我们将在本文档的后面部分说到此文件。

    #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;
};

SEC
("tracepoint/sched/sched_switch")
int 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 0;
}

char _license [] SEC ( "license" ) = "GPL" ;

当该程序使用由内核提供的 BPF 辅助函数时,内核会使用许可证区段来验证该程序是否与内核的许可证兼容。将 _license 设为您项目的许可证。

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 功能:创建和读取映射,以及创建探测点、跟踪点、perf 缓冲区等。

将程序附加到跟踪点和 kprobe

加载跟踪点和 kprobe 程序(如前所述,会在启动时自动完成)后,需要将其激活。要激活它们,请首先使用 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 用户

您可以将 Android 中的两个 eBPF C 程序作为示例来参考。

netd eBPF C 程序可供 Android 中的网络守护程序 (netd) 用于多种用途,例如过滤套接字和收集统计信息。如需了解此程序的使用方式,请查阅 eBPF 流量监控源代码。

time_in_state eBPF C 程序可计算 Android 应用在不同 CPU 频率下运行所花费的时间,该时间可用于计算功耗。此程序目前正在开发中。

许可注意事项

如果您想要贡献 eBPF C 程序,请根据该程序的许可证将其贡献给合适的项目。请将获得 GPL 许可证的 eBPF C 程序贡献给 system/bpfprogs AOSP 项目。将获得 Apache 许可证的程序贡献给  system/bpf AOSP 项目。

点阅读原文跳转到原文

相关文章