在 Android 系统开发或性能调试中,我们常常需要深入内核层面追踪数据,而 eBPF(尤其是其 Map 结构)是关键的观察窗口。这篇文章将通过一个具体的场景——监控进程的 GPU 内存使用量,来演示如何在 Android 设备上查看 BPF Map 中的数据。
理解数据来源:一个 BPF Map 的读取示例
首先,我们来看一段实际用于读取数据的 C++ 代码片段,它清晰地展示了用户空间程序如何与名为 gpu_mem_total_map 的 BPF Map 进行交互。这个 Map 用于存储各进程的 GPU 内存总量。
bool ReadProcessGpuUsageKb([[maybe_unused]] uint32_t pid, [[maybe_unused]] uint32_t gpu_id,
uint64_t* size) {
#if defined(__ANDROID__) && !defined(__ANDROID_APEX__) && !defined(__ANDROID_VNDK__)
static constexpr const char kBpfGpuMemTotalMap[] = "/sys/fs/bpf/map_gpuMem_gpu_mem_total_map";
uint64_t gpu_mem;
// BPF Key [32-bits GPU ID | 32-bits PID]
uint64_t kBpfKeyGpuUsage = ((uint64_t)gpu_id << 32) | pid;
// Use the read-only wrapper BpfMapRO to properly retrieve the read-only map.
auto map = bpf::BpfMapRO<uint64_t, uint64_t>(kBpfGpuMemTotalMap);
if (!map.isValid()) {
LOG(ERROR) << "Can't open file: " << kBpfGpuMemTotalMap;
return false;
}
auto res = map.readValue(kBpfKeyGpuUsage);
if (res.ok()) {
gpu_mem = res.value();
} else if (res.error().code() == ENOENT) {
gpu_mem = 0;
} else {
LOG(ERROR) << "Invalid file format: " << kBpfGpuMemTotalMap;
return false;
}
if (size) {
*size = gpu_mem / 1024;
}
return true;
#else
if (size) {
*size = 0;
}
return false;
#endif
}
从代码中我们可以看到几个关键点:
- BPF Map 在文件系统中的路径是
/sys/fs/bpf/map_gpuMem_gpu_mem_total_map。
- Map 的键(Key)是一个 64 位整数,其高 32 位是 GPU ID,低 32 位是进程 PID。本例中 GPU ID 为 0。
- 值(Value)也是一个 64 位整数,表示以字节为单位的 GPU 内存使用量。
了解数据是如何被存储和读取的之后,我们进入正题:如何绕过应用层,直接使用命令行工具来“窥探”这个 Map 里的原始数据。
bpftool 是 Linux 内核社区维护的 Swiss Army Knife,用于管理和检查 eBPF 对象。在具备 root 权限的 Android 设备上,我们同样可以使用它。假设我们想查看 PID 为 27309 的进程的 GPU 内存使用情况。
首先,我们可以用系统命令 dumpsys gpu 来验证一下这个进程的 GPU 内存总量,作为后续对比的基准。
# dumpsys gpu |grep 27309
Proc 27309 total: 58912768
系统显示该进程的 GPU 内存总量为 58912768 字节。接下来,我们使用 bpftool 来检查对应的 BPF Map。
第一步:查看 Map 的基本信息
使用 bpftool map show 命令可以列出 Map 的类型、大小等元数据。
# bpftool map show pinned /sys/fs/bpf/map_gpuMem_gpu_mem_total_map
62: hash name gpu_mem_total_m flags 0x0
key 8B value 8B max_entries 1024 memlock 16384B
输出显示这是一个哈希表类型的 Map,键和值都是 8 字节(64位),最多可存储 1024 个条目。这验证了代码中对键值类型的定义。
第二步:导出 Map 中的所有数据
使用 bpftool map dump 命令可以将 Map 中的所有键值对以十六进制形式打印出来。
# bpftool map dump pinned /sys/fs/bpf/map_gpuMem_gpu_mem_total_map
key: ad 6a 00 00 00 00 00 00 value: 00 f0 82 03 00 00 00 00
key: d6 08 00 00 00 00 00 00 value: 00 50 44 06 00 00 00 00
key: 00 00 00 00 00 00 00 00 value: 00 d0 d8 0c 00 00 00 00
key: b4 66 00 00 00 00 00 00 value: 00 80 01 00 00 00 00 00
key: b9 66 00 00 00 00 00 00 value: 00 50 a5 02 00 00 00 00
key: 18 69 00 00 00 00 00 00 value: 00 f0 09 00 00 00 00 00
Found 6 elements
输出显示了 6 个条目。我们需要从中找到 PID 为 27309(即 0x6AAD)的那一条。注意,由于系统通常使用小端字节序(Little Endian),我们在读取十六进制时需要从右往左看低有效位。
第三步:解析数据
- 第一个键(Key)的字节序列是
ad 6a 00 00 00 00 00 00。以小端格式解读,其低 32 位是 0x00006aad,换算成十进制正是 27309。高 32 位都是0,对应 GPU ID 0。
- 第一个值(Value)的字节序列是
00 f0 82 03 00 00 00 00。同样以小端格式解读,其数值为 0x000000000382f000,换算成十进制是 58912768。
这个结果与之前 dumpsys gpu 命令的输出完全吻合!至此,我们成功通过直接读取 BPF Map 验证了用户空间获取的数据。
总结与提醒
通过这个具体的例子,我们演示了在 Android 环境下使用 bpftool 工具查看 BPF Map 数据的完整流程:从定位 Map 文件、查看其信息,到导出并解析原始的键值对。这种方法对于调试 eBPF 程序、验证数据准确性,或是进行深度系统排查都非常有用。
记住一个关键点:在解析 bpftool 输出的十六进制数据时,一定要注意字节序问题。大多数现代系统,包括 Android 设备所使用的 ARM 架构,都采用小端字节序,这意味着数值的低位字节存储在内存的低地址处,在打印时也靠前显示。所以我们需要“反着读”才能得到正确的数值。
希望这篇实战指南能帮助你更好地理解和驾驭 Android 系统中的 eBPF 技术。如果你想了解更多关于系统底层或移动开发的技术细节,欢迎来 云栈社区 交流探讨。