Linux BPF map 怎样在用户态读取内核数据
来源:17golang原创
时间:2026-11-03 12:44:25 285浏览 收藏
如果你已经有一个 BPF 程序把统计结果写进 map,用户态读取的核心并不是“直接读内核内存”,而是拿到这个 map 的句柄,用 key 查询 value。Linux 内核文档把 BPF map 定义为内核 BPF 程序与用户态程序共享数据的通用存储;用户态可以通过 BPF 系统调用或 libbpf 访问它。
最稳妥的做法是:让 map 的 key/value 结构在内核侧和用户态保持一致,先通过 libbpf 找到 map,再调用 bpf_map__lookup_elem 或文件描述符接口读取。查不到 key、key/value 尺寸不匹配、per-CPU map 和 map 生命周期,分别是四个最容易混在一起的问题。

先明确用户态读取 BPF map 的数据路径
BPF map 至少有类型、最大元素数、key 大小和 value 大小这些定义。内核侧的 BPF 程序可以用 helper 访问 map,用户态则通过 map 的文件描述符或者 libbpf 对象访问同一个数据容器。两边共享的是约定好的二进制布局,不是 C 变量的名字本身。
因此,读取一次数据可以拆成四个对象:
- map:真正保存 key/value 的内核对象。
- map FD:用户进程持有的文件描述符,低层 BPF 系统调用以它定位 map。
- key:查询入口,长度必须符合 map 的 key 定义。
- value:内核把命中的值复制到用户态缓冲区,缓冲区长度必须符合 map 的 value 定义。
一个常见误区是把 bpf_map_lookup_elem 当成用户态可以直接调用的“内核 helper”。实际上,BPF 程序里的 helper 和用户态的 BPF 系统调用接口属于不同层次;使用 libbpf 时,用户态通常调用 libbpf 的包装函数,由它完成参数检查和系统调用衔接。
用 libbpf 找到 map 并读取普通 value
如果 BPF 对象是通过 libbpf 打开的,最容易维护的路径是先按名称找到 map,再用 bpf_map__lookup_elem。下面的示例只展示读取关系,sample_map 的 key/value 结构需要与 BPF 程序中定义的布局一致。
#include#include #include #include struct sample_key { __u32 pid; }; struct sample_value { __u64 packets; __u64 bytes; }; int read_sample_map(struct bpf_object *obj, __u32 pid) { // 按 BPF 对象中的 map 名称取得用户态句柄,避免手工猜测 FD。 struct bpf_map *map = bpf_object__find_map_by_name(obj, "sample_map"); if (map == NULL) { fprintf(stderr, "找不到 sample_map\\n"); return -1; } struct sample_key key = {.pid = pid}; struct sample_value value = {0}; // key_sz 和 value_sz 必须与 map 创建时的布局一致。 int err = bpf_map__lookup_elem(map, &key, sizeof(key), &value, sizeof(value), 0); if (err != 0) { // -ENOENT 表示这个 key 当前没有元素,其他错误要单独处理。 if (err == -ENOENT) { fprintf(stderr, "pid=%u 暂无数据\\n", pid); } else { fprintf(stderr, "读取失败:%s\\n", strerror(-err)); } return err; } printf("pid=%u packets=%llu bytes=%llu\\n", key.pid, (unsigned long long)value.packets, (unsigned long long)value.bytes); return 0; }
这里的 err 是 libbpf 风格的负错误码。libbpf 文档说明,bpf_map__lookup_elem 会检查 key 和 value 的尺寸;普通 map 命中时返回 0,并把 value 写入用户态缓冲区。示例中的结构体只是表达布局,不能直接替代你自己的 BPF map 定义。
低层文件描述符接口适合什么场景
当程序已经拿到了 map FD,或者需要直接对应内核的 BPF_MAP_LOOKUP_ELEM 命令时,可以使用 libbpf 提供的 FD 包装接口。它的调用形状更接近内核的 BPF 系统调用:传入 FD、key 指针和 value 指针。
#include#include #include int lookup_by_fd(int map_fd, const struct sample_key *key) { struct sample_value value = {0}; // FD 必须仍然指向目标 map,value 缓冲区要按 value_size 准备。 int err = bpf_map_lookup_elem(map_fd, key, &value); if (err != 0) { // 读取不存在的 key 通常表现为 ENOENT,不要把它当成 map 损坏。 if (errno == ENOENT) { return 1; } perror("bpf_map_lookup_elem"); return -1; } printf("packets=%llu bytes=%llu\\n", (unsigned long long)value.packets, (unsigned long long)value.bytes); return 0; }
这条路径的优点是边界直接,缺点是调用方必须自己维护 FD 和结构体布局。使用 BPF skeleton 时,libbpf 会把 map 暴露成 skeleton 的字段,通常比字符串查找更不容易让 BPF 代码和用户态代码失配。
查不到、尺寸错误和并发更新怎么判断
把所有非零返回值统一打印成“读取失败”会让排障变得困难。至少要区分下面几种情况:
| 现象 | 优先判断 | 处理方向 |
|---|---|---|
ENOENT | 这个 key 当前不存在 | 检查 key 是否已经写入,或把它当成正常的空数据分支 |
EINVAL 或尺寸相关错误 | key/value 布局、map 类型或参数不符合要求 | 对照 map 定义检查字节数、对齐和调用 flags |
EPERM | 权限或内核能力不足 | 检查加载、访问 map 所需的权限,不要只重试读取 |
| 读取值变化 | 内核 BPF 程序可能正在更新 map | 根据业务需要使用 per-CPU map、原子字段或快照协议 |
普通 hash 或 array map 的一次 lookup 能保证接口层面的读取结果,但它不自动给多字段业务对象提供“整组快照”语义。如果内核程序在更新 packets 和 bytes 的过程中,用户态恰好读取到中间状态,问题就不在 lookup 调用本身。需要一致性时,应在 value 中设计序列号、版本字段,或使用适合的原子更新方案。
读取 per-CPU map 时先算清缓冲区
per-CPU map 与普通 map 最大的区别,是一个 key 可能对应每个 CPU 的一份 value。libbpf 文档对 bpf_map__lookup_elem 的说明指出:读取 per-CPU value 时,缓冲区通常要按 value 大小乘以可能 CPU 数量准备,并按 8 字节对齐;具体使用单 CPU 还是全部 CPU,还取决于调用 flags。
#include#include int prepare_percpu_value(const struct bpf_map *map) { __u32 value_size = bpf_map__value_size(map); int cpu_count = libbpf_num_possible_cpus(); if (cpu_count
不要把普通 map 的 sizeof(value) 习惯直接套到 per-CPU map 上。是否需要聚合所有 CPU 的值,也取决于你的指标含义:分别展示每核数据时保留数组,展示总量时再按明确的字段规则聚合。

用 map pin 和生命周期保证数据仍可读
map FD 是进程持有的引用。进程退出后,如果没有其他引用,map 的生命周期可能随之结束;Linux 内核文档也说明,关闭 FD 会删除该引用,进程退出时 map 通常会被清理。需要让另一个用户态进程稍后接管时,可以使用 libbpf 的 pin API 把 map 固定到 BPF 文件系统路径,再由后续程序打开这份 pin。
这里要区分两个问题:pin 解决的是“对象还在不在、另一个进程能否找到”的问题,不解决“value 是否已经写入、读取是否是一致快照”的问题。长期观测程序通常需要同时设计 map 的 pin 路径、访问权限、升级时的兼容策略,以及旧 map 的清理策略。
一张表记住用户态读取的检查顺序
| 检查项 | 要确认的内容 |
|---|---|
| map 句柄 | 名称、skeleton 字段或 FD 确实指向目标 map |
| key | 字节布局、长度和字节序与 BPF 侧约定一致 |
| value | 普通 map 按 value_size 分配,per-CPU map 额外考虑对齐和 CPU 数 |
| 返回值 | 把 ENOENT、参数错误和权限错误分开处理 |
| 一致性 | 多字段读数是否需要版本号、原子更新或聚合策略 |
| 生命周期 | 临时 FD 还是 pin 后跨进程复用,并明确清理责任 |
常见延伸问题
用户态能直接读取 BPF 程序里的局部变量吗?
不能把局部变量当作公开接口。需要用户态读取的数据,应放进可访问的 BPF map 或全局数据区域,并明确其布局和生命周期。
为什么 key 明明存在却返回 ENOENT?
先检查 key 的每个字节是否一致,包括整数宽度、填充字节、字节序和字符串是否带结尾零。map 类型也可能改变 key 的解释方式。
读取 map 前一定要自己调用 bpf 系统调用吗?
不一定。libbpf 已经提供了查找、读取、更新和遍历的包装接口;直接使用系统调用只适合需要更底层控制或维护既有 FD 管理代码的场景。
总的来说,用户态读取 BPF map 的关键是把“句柄、key、value、生命周期”四件事对齐。普通 map 先用 libbpf 查找并 lookup;per-CPU map 先计算正确缓冲区;出现空结果时优先判断 key 是否存在;需要跨进程继续访问时再把 pin 纳入生命周期设计。
-
Golang · Go教程 | 3星期前 | CGO · 资源管理 · Go教程 · runtime.KeepAlive runtime/cgo.Handle Go cgo C库句柄 LockOSThread400 收藏
-
356 收藏
-
307 收藏
-
236 收藏
-
385 收藏
-
166 收藏
-
148 收藏
-
432 收藏
-
227 收藏
-
308 收藏
-
383 收藏
-
413 收藏
-
437 收藏
-
247 收藏
-
157 收藏
-
263 收藏
-
391 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习