本页面简要介绍了 Android 中的扩展伯克利数据包过滤器 (eBPF) 内核遥测框架,并介绍了框架架构、内置指标以及如何添加新的内核指标。
Android 包含一个统一的 eBPF 内核遥测框架,用于收集低开销内核指标,并将这些指标公开给 statsd 等用户空间服务。通过将可移植的 eBPF 程序挂接到内核跟踪点和
kprobe,该框架可以在本地聚合高频内核事件,并
导出紧凑的直方图或计数器,而无需修改
通用内核映像 (GKI) 或破坏内核模块接口 (KMI)。
传统的内核遥测方法依赖于自定义 procfs 或 sysfs 节点、树外内核补丁或供应商钩子,这些方法需要在内核、SELinux、Java 原生接口 (JNI) 和用户空间服务之间进行自定义管道。
eBPF 内核遥测框架在 Android 通用内核 (ACK) 中标准化了此管道,并提供了以下优势:
- 避免内核 ABI 和 KMI 变动: 将 eBPF 程序挂接到现有内核跟踪点或 kprobe,而无需修改内核源代码、更改内核应用二进制接口 (ABI) 或添加
TRACE_ANDROID_VH_*等 Android 供应商钩子。设备制造商和系统芯片 (SoC) 合作伙伴可以使用 eBPF 遥测来代替添加新的遥测供应商钩子,或迁移掉现有供应商钩子。 - 保持跨内核版本的可移植性: 使用 BPF Compile Once - Run Everywhere (CO-RE) 和 BPF Type Format (BTF),以便单个编译的 eBPF 程序自动适应 GKI 版本(例如
android14-6.1及更高版本)中内部内核结构布局的变化。 - 最大限度地减少系统健康状况开销: 直接在源中过滤和聚合高频内核事件,将其转换为紧凑的 32 存储桶以 2 为底的对数 (
log2) 直方图,通常每个 BPF 映射小于 1 KB。 用户空间每天使用单个BPF_MAP_LOOKUP_BATCH批量系统调用检索聚合的映射条目。 - 简化 SELinux 和 JNI 集成: 将遥测映射固定在
/sys/fs/bpf/kernelstats/下,以应用目录级fs_bpf_kernelstatsSELinux 类型继承,并使用 BTF 元数据在 JNI 中运行时解析结构成员偏移量,从而在添加新指标时消除自定义 C++ 解析代码。
如需详细了解 Android 如何加载和固定 eBPF 程序和映射, 请参阅 使用 eBPF 扩展内核。
架构概览
eBPF 内核遥测管道将通用基础架构组件与特定于指标的定义分开,以便添加新的内核指标只需进行最少的代码更改。下表介绍了管道的组件:
| 组件 | 在管道中的角色 | 范围 |
|---|---|---|
BPF 加载器,bpfloader | 如果与正在运行的内核兼容,则在启动时从 /system/etc/bpf/kernelstats/ 加载已编译的 eBPF 程序,并将映射固定到 /sys/fs/bpf/kernelstats/。如果已停用或不受支持,则会干净利落地跳过加载。 | 通用 |
SELinux 政策,fs_bpf_kernelstats | 将目录级 SELinux 标签应用于 /sys/fs/bpf/kernelstats/,以便 system_server 可以读取遥测映射,而无需针对每个指标更新 SELinux 政策。 | 通用 |
system/bpfprogs/telemetry/ 中的 eBPF 程序 | 挂接到内核跟踪点或 kprobe,过滤事件,并将指标聚合到 BPF 映射中。 | 每个指标的自定义 |
| JNI BTF 读取器 | 在运行时从已编译的 BPF 可执行文件和可链接格式 (ELF) 二进制文件解析 BTF 元数据,缓存结构成员偏移量,并批量将映射读取到原始 Java 数组中。 | 通用 |
statsd 中的用户空间提取器 | 按每日计划提取累积直方图映射,根据缓存的历史记录计算增量,并构建强类型 StatsEvent 提取原子。 | 通用帮助程序,每个原子都有一个 case 条目 |
| 遥测架构 | 定义强类型 statsd 提取原子 protobuf 和服务器端直方图或值指标聚合。 | 每个指标的自定义 |
用户空间使用模式
在设计内核遥测指标时,请根据指标是表示累积状态还是有序事件序列,在两种用户空间使用模式之间进行选择。下表比较了这两种模式:
| 属性 | 累加器 | 数据流 |
|---|---|---|
| 定义 | 有状态、单调递增的累积计数器或直方图 | 带时间戳的内核事件的有序序列 |
| 访问模式 | 用户空间按需轮询,例如通过 statsd 每 24 小时轮询一次 | 用户空间从环形缓冲区持续使用,以避免事件丢失 |
| 典型的 BPF 映射类型 | BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_LRU_HASH、BPF_MAP_TYPE_PERCPU_HASH、BPF_MAP_TYPE_LRU_PERCPU_HASH | BPF_MAP_TYPE_RINGBUF |
| 并发模型 | 内核映射查找是原子的;多个读取器可以轮询,而无需用户空间锁定 | 破坏性读取语义需要单个读取器线程调度到使用者 |
| 应用场景示例 | 内存管理延迟直方图、直接内存访问缓冲区 (DMA-BUF) 分配大小、唤醒源迭代器 | 经过过滤的调度程序跟踪事件、高频子系统电源跟踪 |
内置内核遥测指标
Android 包含内置的 eBPF 内核遥测程序,这些程序适用于 GKI 6.1 及更高版本的内核,包括 android14-6.1、android15-6.6、android16-6.12 和 android17-6.18。每个延迟和大小指标都使用在 24 小时窗口内聚合的 32 存储桶指数 (log2) 直方图。下表列出了内置内核遥测指标:
| 指标原子 | 子系统 | 说明 | 维度键 |
|---|---|---|---|
KswapdLatency | 内存管理 | 内核交换守护程序 kswapd 的唤醒到休眠执行延迟时间(以微秒为单位) | 非统一内存访问 (NUMA) node_id |
DirectReclaimLatency | 内存管理 | 同步直接内存回收延迟时间(以微秒为单位) | 进程内存不足 (OOM) oom_score_adj 状态 |
CompactionDuration | 内存管理 | 系统级内存压缩执行时长(以微秒为单位),取代了旧版压缩供应商钩子 | 系统级 |
CmaAllocationLatency | 内存管理 | 连续内存分配器 (CMA) 页面分配延迟时间(以微秒为单位) | CMA 堆名称 |
DmabufRequestSizes | 内存管理 | DMA-BUF 分配请求大小(以字节为单位) | 导出器堆名称 |
SuspendResumeLatency | 电源管理 | 内核暂停和恢复阶段的延迟时间(以微秒为单位) | 暂停操作阶段 |
添加内核遥测指标
如需向 eBPF 内核遥测管道添加新的累加器指标,请按顺序完成以下步骤:
- 编写 eBPF 程序,位置在
system/bpfprogs/telemetry/下,并将二进制文件构建到/system/etc/bpf/kernelstats/中。 - 验证 BTF 结构体布局兼容性,适用于 通用 JNI BTF 读取器。
- 在
statsd中注册提取原子。 - 定义提取原子 protobuf 架构。
以下部分以 DirectReclaimLatency (directReclaim.c) 为例。
编写 eBPF 程序
在 system/bpfprogs/telemetry/ 中,添加 <bpf_telemetry.h> 并定义映射
键和输出累加器映射。在 BPF 映射键中使用整数标识符或枚举,而不是原始字符串,以最大限度地减少映射内存占用并保护隐私:
#include <android_bpf_defs.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf_telemetry.h>
#include <stdint.h>
struct direct_reclaim_key {
uint32_t process_state;
};
#define DIRECT_RECLAIM_START_MAP_ENTRIES 128
#define DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES 8
// Temporary map to store start time. Key: pid (thread_id), Value: start_time_ns
DEFINE_BPF_MAP_GRO(direct_reclaim_start_map, HASH, uint32_t, uint64_t,
DIRECT_RECLAIM_START_MAP_ENTRIES, AID_SYSTEM);
// Output map. Key: direct_reclaim_key, Value: accumulator_value (32 log2 buckets)
DEFINE_BPF_MAP_GRO(direct_reclaim_latency_map, HASH, struct direct_reclaim_key,
struct accumulator_value, DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES,
AID_SYSTEM);
在跟踪点处理程序中,记录条目中的开始时间戳,使用 GET_LATENCY_BUCKET 计算退出时的 log2 延迟时间存储桶,并使用 INCREMENT_ACCUMULATOR 递增累加器映射条目:
DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_begin", AID_ROOT, AID_SYSTEM,
tracepoint_vmscan_mm_vmscan_direct_reclaim_begin)(
uint64_t* args) {
(void)args;
uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
SAVE_FIRST_START_TIME(direct_reclaim_start_map, &pid);
return 0;
}
DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_end", AID_ROOT, AID_SYSTEM,
tracepoint_vmscan_mm_vmscan_direct_reclaim_end)(
uint64_t* args) {
(void)args;
uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
uint32_t bucket = GET_LATENCY_BUCKET(direct_reclaim_start_map, &pid);
if (bucket == BPF_TELEMETRY_INVALID_BUCKET) return 0;
struct task_struct* task = (struct task_struct*)bpf_get_current_task();
int oom_score_adj = BPF_CORE_READ(task, signal, oom_score_adj);
enum process_state state = get_process_state(oom_score_adj);
struct direct_reclaim_key key = {.process_state = (uint32_t)state};
INCREMENT_ACCUMULATOR(direct_reclaim_latency_map, &key, bucket);
return 0;
}
验证 BTF 结构布局兼容性
添加新的直方图映射时,只要您的映射键和值结构使用标准整数、枚举和固定数组类型,就不需要编写自定义 JNI C++ 代码。通用 JNI 读取器不会在编译时将 BPF 映射条目转换为硬编码的 C 结构,而是在运行时分三个阶段动态解析结构布局:
- ELF 到映射关联: JNI 层使用
libbpfbtf__parse函数直接从磁盘上已编译的 BPF ELF 文件(例如/system/etc/bpf/kernelstats/directReclaim.bpf)解析 BTF 元数据。 - BTF 类型解析: 首次访问时,读取器会构建并缓存映射的键和值 C 结构体的结构表示法,从基指针计算结构体成员的累积字节偏移量、整数和枚举字节宽度以及数组元素大小。
- 批量查找和序列化: 当
statsd提取映射时,JNI 层会向sys_bpf系统调用发出单个BPF_MAP_LOOKUP_BATCH命令,以批量读取所有哈希条目,并使用SetLongArrayRegionJNI 函数将解析的键 ID 和存储桶数组直接复制到原始 Javalong[]数组中。
如果 BPF C 结构体在未来的版本中发生变化,重新编译的 BTF 部分会自动更新,并且 JNI 读取器会在运行时进行调整,而无需重新编译 JNI。
在 statsd 中注册提取原子
在 StatsPullAtomService.java 中,通用 pullKernelTelemetry 帮助程序会读取固定的 BPF 映射,根据上一次提取的缓存计数计算存储桶增量,并发出 StatsEvent 条目。添加新的内核遥测指标只需要进行三项小改动:
// 1. Dispatch the atom tag to pullKernelTelemetry() in onPullAtomLocked():
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY:
return pullKernelTelemetry(atomTag, data);
// 2. Add the atom tag to the registered atoms list in registerKernelTelemetry():
int[] atoms = {
FrameworkStatsLog.DIRECT_RECLAIM_LATENCY,
// ...
};
// 3. Map the atom tag to its pinned BPF map path:
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY ->
"/sys/fs/bpf/kernelstats/map_directReclaim_direct_reclaim_latency_map";
定义提取原子 protobuf
在 frameworks/proto_logging/ 中定义强类型提取原子消息,以匹配 BPF 映射生成的维度键和 32 存储桶 log2 直方图数组:
message DirectReclaimLatency {
enum ProcessState {
PROCESS_STATE_UNSPECIFIED = 0;
STATE_NATIVE = 1;
STATE_SYSTEM = 2;
STATE_PERSISTENT = 3;
STATE_FOREGROUND = 4;
STATE_VISIBLE = 5;
STATE_PERCEPTIBLE = 6;
STATE_SERVICE_BACKGROUND = 7;
STATE_CACHED = 8;
}
optional ProcessState process_state = 1;
repeated int32 duration_log2_buckets_micros = 2;
}