当前位置:首页 > 文章列表 > 文章 > linux > Linux eBPF map保存内核统计

Linux eBPF map保存内核统计

来源:17golang原创 2026-11-15 16:39:20 0浏览 收藏

如果你需要让 eBPF 程序持续保存包数、字节数或错误次数,最直接的做法不是把结果写进日志,而是定义一个 BPF map:内核程序按约定的 key 找到 value 并更新,用户态程序再通过 map 的文件描述符读取和聚合。对高频计数,优先考虑 per-CPU map;对按连接、进程或其他动态键保存状态,再考虑 HASH。

官方地址:https://www.kernel.org/doc/html/latest/bpf/maps.html

把 map 当成“内核程序与用户态共同遵守的数据结构契约”:先固定 key/value 布局,再选择 map 类型,最后明确用户态读取时是否需要合并每个 CPU 的副本。

先分清 map 保存的是什么

Linux 内核文档把 BPF map 定义为可在 eBPF 程序之间、以及内核与用户态之间共享的通用存储。它至少有类型、最大元素数、key 大小和 value 大小等属性;不同类型还会增加自己的语义。

因此,下面三类需求不要混在一起:

需求更适合的方向关键边界
固定下标的累计计数BPF_MAP_TYPE_ARRAY 或 per-CPU 变体key 空间固定,max_entries 决定容量
按进程、连接或对象保存状态BPF_MAP_TYPE_HASH 或 LRU 变体动态 key 需要关注容量和淘汰策略
把事件连续送到用户态ring buffer、perf event array 等专用 map它们是事件通道,不等同于可随时 lookup 的计数表

本文只沿着前两类“保存统计值”的路径展开,事件流的消费模型另有一套接口。

用 BTF 风格声明 key 与 value

现代 libbpf 代码通常把 map 放在 .maps 段,并用 __type 描述 key/value。这样加载器能够保留 BTF 类型信息,用户态生成的 skeleton 也更容易与结构体保持一致。

// 这个 value 同时保存包数和字节数,字段顺序是内核与用户态的共同契约
struct traffic_stat {
    __u64 packets;
    __u64 bytes;
};

// 固定 CPU 下标的统计表;每个 key 对应一个固定位置
struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __type(key, __u32);
    __type(value, struct traffic_stat);
    __uint(max_entries, 64); // 只允许 0 到 63 的 CPU 统计槽位
} traffic_stats SEC(".maps");

这段声明表达了四个事实:map 类型是 per-CPU array,key 是 32 位整数,value 是两个 64 位计数器,最多保留 64 个 key。它没有自动探测机器 CPU 数,也没有承诺 64 个槽位一定都被使用;生产代码应让容量与采集对象的实际范围一致。

eBPF map从内核程序保存per-CPU统计并供用户态读取的静态结构图
图1:说明图展示 eBPF map 保存内核统计并向用户态提供读取边界的静态关系,不是截图或运行证据。

用 per-CPU map 保存高频计数

多个 CPU 同时更新一个共享计数器时,需要考虑竞争和同步开销。per-CPU map 为每个逻辑 CPU 保留自己的 value 副本:当前 CPU 上运行的 eBPF 程序更新本地副本,用户态读取时再把各副本相加。

内核侧更新可以保持很小:

// 在挂载点统计一次数据包;真实项目应按上下文选择可用字段
SEC("tracepoint/net/netif_receive_skb")
int count_packet(void *ctx)
{
    __u32 key = 0;
    struct traffic_stat *stat;

    // lookup 返回当前 CPU 对应的 value,找不到时不要解引用空指针
    stat = bpf_map_lookup_elem(&traffic_stats, &key);
    if (!stat) {
        return 0; // 统计失败不应影响被观测的内核路径
    }

    // per-CPU 副本只由当前 CPU 更新,避免在这里做跨 CPU 汇总
    stat->packets += 1;
    stat->bytes += 1500; // 示例固定值仅表达字段更新,不代表真实包长
    return 0;
}

这里的固定 1500 只是为了突出 value 的更新位置,不能当作真实包长。实际程序应从当前 hook 的上下文或合适 helper 中取得长度,并确认该上下文允许调用相应 helper。

如果统计对象是动态的,例如按进程 ID 保存,使用 HASH 更自然:

// 动态 key 适合按进程或连接保存状态,但容量需要提前规划
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);              // 例如进程 ID
    __type(value, struct traffic_stat);
    __uint(max_entries, 4096);       // 达到容量后不能继续假设有无限新 key
} process_stats SEC(".maps");
PERCPU_ARRAY与HASH在键空间和用户态聚合边界上的静态对比图
图2:结构图对比 PERCPU_ARRAY 与 HASH 的静态数据组织方式,并标出用户态聚合与容量边界,不代表某次命令的真实输出。

用户态读取时要先识别 value 形态

普通 ARRAY 或 HASH 的一次 lookup 通常得到一个 value;per-CPU map 则对应多个 CPU 副本。用户态读取库会把这些副本暴露成按 CPU 排列的数据,应用层必须遍历并累加,而不能只取第一个元素。

// 伪代码展示读取边界;具体函数名以使用的 libbpf 封装为准
__u32 key = 0;
struct traffic_stat *per_cpu_values = NULL;
__u64 packets = 0;
__u64 bytes = 0;

// 读取 key 对应的所有 CPU 副本,并取得当前 CPU 数量
int err = read_percpu_value(map_fd, &key, &per_cpu_values, &cpu_count);
if (err != 0) {
    // 把“没有 key”和“系统调用失败”分别记录,便于排查采集空洞
    report_lookup_error(err);
    return;
}

for (int cpu = 0; cpu 

上面的 read_percpu_value 是为了说明接口边界的占位函数,不是某个固定库的 API。使用 libbpf、Go 封装或其他加载器时,应查对应版本的读取函数和内存布局。文章真正要保留的是:读取成功后遍历每个 CPU,聚合前不要把单个副本当成全机总量。

容量、生命周期和一致性边界

容量:max_entries 是 map 的设计约束。ARRAY 的 key 空间固定,HASH 还要考虑动态 key 进入后是否可能达到上限;如果业务需要淘汰,应在建表时选择合适的 LRU 变体,而不是等满表后再补救。

生命周期:map 最终由用户态加载器创建并持有文件描述符。普通打开的 map 在相关文件描述符关闭后会进入清理路径;如果希望程序重启后继续找到同一个对象,需要设计 pinning 和固定的 bpffs 路径,并处理旧对象与新结构不兼容的情况。

一致性:用户态把多个 CPU 副本逐个读取并相加时,只能把结果理解成一个采样窗口内的近似快照。内核计数可能正在继续变化;如果业务要求严格的事务性快照,就不能仅凭一次 map lookup 宣称所有字段来自同一时刻。

布局:内核和用户态必须对 key/value 的大小、字段顺序和对齐有共同理解。跨语言读取时,不要直接把语言层面的对象布局当成 BPF value 布局,应该使用明确的定长字段和共享定义。

一条实用的选型口诀

固定下标的总量用 ARRAY;多个 CPU 高频写入用 PERCPU_ARRAY;动态对象键用 HASH;需要把一串事件送到用户态则另选 ring buffer 或 perf event array。无论选择哪一种,先写清楚四件事:key 是什么、value 有多大、最多多少条、用户态是否需要聚合。

当统计结果异常时,按这个顺序排查:先确认 map 类型与 key 是否匹配,再确认 lookup 是否真的成功,然后确认 per-CPU 副本是否全部累加,最后检查 max_entries、pinning 和用户态结构体布局。这样比先怀疑 eBPF 程序“没有执行”更容易找到问题所在。

常见疑问

eBPF map 能不能直接当日志队列?

不应把所有 map 都当日志队列。ARRAY、HASH 更适合可按 key 查询的状态;连续事件应使用专用事件通道,并采用对应的消费模型。

为什么读取 per-CPU map 后数值变大了?

因为用户态看到的是多个 CPU 副本。把它们相加得到的是主机范围总量;如果只想看某个 CPU,应只读取对应副本。

HASH 满了以后会自动扩容吗?

不能把自动扩容当作 map 的默认能力。创建时的 max_entries 是边界,动态键很多时应提前估算容量,或选择带淘汰语义的 map 类型。

总结

Linux eBPF map 的核心价值是把内核侧采集与用户态消费连接成一个明确的数据契约。用 BTF 风格声明 key/value,用 per-CPU map 承载高频计数,再在用户态完整读取并聚合,便能得到可维护的统计路径。最后别忘了把 map 类型、容量、生命周期和快照一致性写进设计说明,否则“能读到数”并不等于“数就能被正确解释”。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Linux BPF CO-RE适配不同内核Linux BPF CO-RE适配不同内核
上一篇
Linux BPF CO-RE适配不同内核
Linux systemd定时器替代cron任务
下一篇
Linux systemd定时器替代cron任务
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    1039次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1038次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1181次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1143次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    987次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码