当前位置:首页 > 文章列表 > 文章 > linux > Linux BPF map 怎样在用户态读取内核数据

Linux BPF map 怎样在用户态读取内核数据

来源:17golang原创 2026-11-03 12:44:25 0浏览 收藏

如果你已经有一个 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 程序通过 BPF map 和 map FD 与用户态 libbpf 交换 key 和 value 的结构说明图
图1:BPF 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 与 per-CPU map 的 value 大小和 map pin 生命周期边界说明图
图2:per-CPU map 缓冲区与 map 生命周期的静态结构图,帮助理解读取空间和持久引用,不是运行截图。

用 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 纳入生命周期设计。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Linux signalfd 怎样把信号转成文件描述符事件Linux signalfd 怎样把信号转成文件描述符事件
上一篇
Linux signalfd 怎样把信号转成文件描述符事件
Linux nftables 怎样按接口限制入站端口
下一篇
Linux nftables 怎样按接口限制入站端口
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    857次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    862次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    938次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    919次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    747次使用