Linux 内核 BPF_MAP_TYPE_CGROUP_STORAGE 深度解析:cgroup 本地存储的用法、语义与 5.9 版本行为变化
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
BPF_MAP_TYPE_CGROUP_STORAGE是 eBPF 为 cgroup 场景提供的"本地定长存储"map 类型:以 BPF 程序所附着的 cgroup 为标识,为该 cgroup 分配一块私有内存,让 cgroup BPF 程序无需借助通用哈希表即可高速、简洁地读写与特定 cgroup 绑定的状态。本文基于内核仓库中的 官方文档,完整覆盖该 map 的 key 类型、bpf_get_local_storage调用方式、Linux 5.9 前后的生命周期语义差异与用户态访问限制,并结合 kernel/bpf/local_storage.c、kernel/bpf/cgroup.c、kernel/bpf/syscall.c 的源码实现,说明其分配时机、per-CPU 变体的内存布局与自旋锁同步机制,帮助你在编写 cgroup BPF 程序时正确选择 key 类型并规避 5.9 版本的行为陷阱。
基本概念与启用条件
BPF_MAP_TYPE_CGROUP_STORAGE表示一种"local fix-sized storage"(本地定长存储)。根据 Documentation/bpf/map_cgroup_storage.rst 的定义,它有两个硬性前提:
- 配置前提:只有在内核配置了
CONFIG_CGROUP_BPF时才可用。同名的 Kconfig 选项同时启用"可附着到 cgroup 的 BPF 程序"能力,即该 map 类型与 cgroup BPF 程序能力是同一开关控制的; - 附着前提:只有附着(attach)到 cgroup 上的 BPF 程序才能使用它,存储以"程序所附着的 cgroup"为标识。
它相对通用哈希表(如BPF_MAP_TYPE_HASH)的优势有两点:一是访问更快——无需哈希表查找,内核直接按附着关系定位存储;二是使用更简单——哈希表方案要求用户自己跟踪"哪些 cgroup 还活着"并手动维护条目,而 CGROUP_STORAGE 由内核在附着/分离生命周期内自动管理存储的创建与销毁。
此外还有一个变体BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE:per-CPU 变体为每个存储的每个 CPU 各自维护一块独立内存区域,而普通变体对每个存储只有一块共享内存区域。这一差异在用户态访问时有可验证的量化依据——kernel/bpf/syscall.c 中bpf_map_value_size()对 per-CPU 变体返回round_up(value_size, 8) * num_possible_cpus(),即实际传输的 value 按 8 字节对齐后乘以所有可能 CPU 的数量。
key 类型:两种取法与共享语义
该 map 的 key 支持两种类型(均可在 include/uapi/linux/bpf.h 中找到定义):
struct bpf_cgroup_storage_key { __u64 cgroup_inode_id; /* cgroup inode id */ __u32 attach_type; /* program attach type (enum bpf_attach_type) */ };cgroup_inode_id:cgroup 目录的 inode id;attach_type:程序的附着类型(enum bpf_attach_type)。
两种 key 类型的选择直接决定存储的共享粒度:
| key 类型 | 存储隔离粒度 | 引入版本 |
|---|---|---|
__u64 cgroup_inode_id | 同一 cgroup + 同一 map 下,所有 attach type 共享同一块存储 | Linux 5.9 |
struct bpf_cgroup_storage_key | 不同 attach type 的程序互相隔离,各自看到不同的存储 | 5.9 之前即支持 |
也就是说,Linux 5.9 新增的__u64 cgroup_inode_id简化了"多个附着类型希望共享同一份数据"的场景:此时内核在比较时只使用 key 的 cgroup inode id,attach type 被忽略。
程序内访问:bpf_get_local_storage
在 BPF 程序中访问存储统一通过 helperbpf_get_local_storage完成(其 uapi 声明见 include/uapi/linux/bpf.h):
void *bpf_get_local_storage(void *map, u64 flags)flags字段保留给未来使用,当前必须传 0。返回指针即该 cgroup 存储的 value 首地址。
注意:内核不提供任何隐式同步。同一块 CGROUP_STORAGE 可以被跨 CPU 的多个程序并发访问,并发安全需要程序员自行保证。BPF 基础设施为此提供struct bpf_spin_lock同步原语,仓库中可直接参考 tools/testing/selftests/bpf/progs/test_spin_lock.c 的用法。从源码结构看,BPF_SPIN_LOCK字段类型在 map 创建时被明确列入 CGROUP_STORAGE 的白名单——kernel/bpf/syscall.c 中只有 HASH、RHASH、ARRAY、CGROUP_STORAGE、SK_STORAGE、INODE_STORAGE、TASK_STORAGE 等类型允许携带自旋锁字段,其余类型返回-EOPNOTSUPP。用户态读取带自旋锁的 map 元素时同样需要带BPF_F_LOCK标志。
示例一:以 struct bpf_cgroup_storage_key 为 key
以下 BPF 侧代码完整继承自 官方文档,声明了一个以bpf_cgroup_storage_key为 key、__u32为 value 的 CGROUP_STORAGE map,并在程序中对存储做原子自增:
#include <bpf/bpf.h> struct { __uint(type, BPF_MAP_TYPE_CGROUP_STORAGE); __type(key, struct bpf_cgroup_storage_key); __type(value, __u32); } cgroup_storage SEC(".maps"); int program(struct __sk_buff *skb) { __u32 *ptr = bpf_get_local_storage(&cgroup_storage, 0); __sync_fetch_and_add(ptr, 1); return 0; }对应的用户态访问代码:以"inode id + attach type"二元组构造 key,调用bpf_map_lookup_elem读取指定附着点的存储:
#include <linux/bpf.h> #include <linux/libbpf.h> __u32 map_lookup(struct bpf_map *map, __u64 cgrp, enum bpf_attach_type type) { struct bpf_cgroup_storage_key key = { .cgroup_inode_id = cgrp, .attach_type = type, }; __u32 value; bpf_map_lookup_elem(bpf_map__fd(map), &key, &value); // error checking omitted return value; }示例二:以 __u64 cgroup_inode_id 为 key(5.9+ 共享模式)
如果希望同一 cgroup 下所有 attach type 共享一份存储,key 直接声明为__u64:
#include <bpf/bpf.h> struct { __uint(type, BPF_MAP_TYPE_CGROUP_STORAGE); __type(key, __u64); __type(value, __u32); } cgroup_storage SEC(".maps"); int program(struct __sk_buff *skb) { __u32 *ptr = bpf_get_local_storage(&cgroup_storage, 0); __sync_fetch_and_add(ptr, 1); return 0; }用户态则无需构造结构体,直接把 cgroup inode id 作为 key 传入即可(原文示例保留了type参数签名,但 5.9 共享语义下内核只比较第一个值):
#include <linux/bpf.h> #include <linux/libbpf.h> __u32 map_lookup(struct bpf_map *map, __u64 cgrp, enum bpf_attach_type type) { __u32 value; bpf_map_lookup_elem(bpf_map__fd(map), &cgrp, &value); // error checking omitted return value; }生命周期语义:Linux 5.9 前后的关键差异
这是本文档最核心的技术内容,也是跨内核版本迁移时最容易踩坑的地方。
5.9 之前:每附着点一份存储,map 与程序一对一
- 存储的生命周期精确到每次附着(per-attachment)。一个 CGROUP_STORAGE map 在同一时刻最多只能被一个已加载程序使用;
- 程序可以附着到多个 cgroup、或拥有多个 attach type,每一次附着都会新建一块清零的存储;
- 分离(detach)时存储立即释放;
- 这种一对一绑定导致无法在多个 BPF 程序之间共享同一 cgroup 的存储。
5.9 起:存储可被多程序共享
- 程序附着到 cgroup 时,内核仅在"map 中尚不存在 (cgroup, attach type) 对应条目"时创建新存储,否则复用旧存储;若 map 采用共享 key(
__u64),则比较时直接忽略 attach type; - 分离不再直接释放存储。存储只在两种情况下被释放:map 本身被销毁,或所附着的 cgroup 被销毁;
- detach 只是让 map 的引用计数减少,可能间接使 map 引用归零从而释放 map 内全部存储。
从源码可以印证这一附着期分配行为:kernel/bpf/cgroup.c 在 cgroup BPF 程序附着路径中调用bpf_cgroup_storage_alloc(prog, stype)分配存储,而该函数的实现位于 kernel/bpf/local_storage.c。
但注意 5.9 并未放开"一个程序用多个存储 map"的限制:一个 BPF 程序最多只能关联一个BPF_MAP_TYPE_CGROUP_STORAGE,也最多只能关联一个BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE(两类各限一个)。放开的是 map 到程序的共享方向,而不是程序到 map 的引用方向。
附着点绑定规则
存储在 attach 时刻绑定。即使程序附着在父 cgroup 上、而触发点发生在子 cgroup,所用存储依然属于父 cgroup。这意味着"在触发事件的 cgroup 上计数"这类需求不能依靠 CGROUP_STORAGE 自动获得,需要显式附着到目标层级。
用户态操作限制
跨所有内核版本,用户态通过 BPF map API 操作该 map 时有两条硬限制(原文以struct bpf_cgroup_storage_key形式表述的"附着参数"在 5.9 共享模式下退化为只比较 cgroup inode id,因此可以直接传__u64):
- 不能创建新条目,也不能删除已有条目。条目的增删完全由内核在附着/分离与 cgroup 销毁流程中驱动;用户态仅能对这些内核管理的条目执行 lookup / update(读取或更新存储内容);
- 程序 test run(
BPF_PROG_TEST_RUN)始终使用一块临时存储,不会触碰真实 cgroup 的存储,可用于安全地单元测试程序逻辑。
实践要点小结
- 需要"每个 cgroup 一份私有计数器/状态"且要求高性能时,优先选择 CGROUP_STORAGE 而非 HASH 表,省去手动生命周期管理;
- 多个程序要共享同一 cgroup 数据、且运行在 5.9+ 内核上时,用
__u64key;需要在不同 attach type 之间隔离状态时,用struct bpf_cgroup_storage_key; - 跨 CPU 并发写同一存储时必须自行加锁(
struct bpf_spin_lock),并参考 selftest 示例 的锁使用方式; - 依赖"detach 即清零"的旧逻辑在内核升级到 5.9 后不再成立,升级时务必检查是否依赖了 5.9 前的 per-attachment 语义。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考