Linux BPF Map-of-Maps 深度解析:ARRAY_OF_MAPS 与 HASH_OF_MAPS 的设计、约束与源码实现

发布时间:2026/9/14 19:10:29
Linux BPF Map-of-Maps 深度解析:ARRAY_OF_MAPS 与 HASH_OF_MAPS 的设计、约束与源码实现 Linux BPF Map-of-Maps 深度解析ARRAY_OF_MAPS 与 HASH_OF_MAPS 的设计、约束与源码实现【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文基于 Linux 内核源码树中 Documentation/bpf/map_of_maps.rst 官方文档展开系统讲解BPF_MAP_TYPE_ARRAY_OF_MAPS与BPF_MAP_TYPE_HASH_OF_MAPS两类map 套 map结构的用途、约束与两种典型用法BPF 程序内查询 用户态 libbpf 编程并结合 kernel/bpf/syscall.c 的实现代码与 tools/testing/selftests/bpf/progs/test_btf_map_in_map.c 自测用例说明内核侧的元素存储、fd 管理与 RCU 同步细节。读完本文你将能够在内核 BPF 程序中以 BTF 声明式方式初始化 map-of-maps、在用户态正确创建外层 map 并填充内层 map并理解其背后的生命周期与内存模型。概念与整体约束BPF_MAP_TYPE_ARRAY_OF_MAPS和BPF_MAP_TYPE_HASH_OF_MAPS在内核 4.12 中引入提供通用的map in mapmap-of-maps存储能力。其核心特征是只支持一层嵌套外层 mapouter map中的每个元素都是内层 mapinner map实例且所有内层 map 必须是同一种类型。官方文档列出的硬性约束这些约束在内核验证路径中都有对应检查不支持多级嵌套即 inner map 本身不能又是 map-of-maps任何 BPF map 类型都可以作为 inner map唯独BPF_MAP_TYPE_PROG_ARRAY除外BPF 程序只能对外层 map 做元素查找lookup不能更新或删除外层条目外层条目的增删改只允许用户态通过 syscall API 完成创建外层 map 时必须提供一个inner map 实例作为元数据模板。该模板只用于初始化外层 map 所持有的 inner map 元信息类型、key/value 大小、max_entries 等它与外层 map 生命周期相互独立外层 map 创建完成后即可被销毁。从 kernel/bpf/syscall.c 的源码结构看内核把外层 map 归类为fd 类 map这是理解其存储模型的关键#define IS_FD_ARRAY(map) ((map)-map_type BPF_MAP_TYPE_PERF_EVENT_ARRAY || \ (map)-map_type BPF_MAP_TYPE_CGROUP_ARRAY || \ (map)-map_type BPF_MAP_TYPE_ARRAY_OF_MAPS) #define IS_FD_HASH(map) ((map)-map_type BPF_MAP_TYPE_HASH_OF_MAPS) #define IS_FD_MAP(map) (IS_FD_ARRAY(map) || IS_FD_PROG_ARRAY(map) || \ IS_FD_HASH(map))即ARRAY_OF_MAPS与BPF_MAP_TYPE_PERF_EVENT_ARRAY、BPF_MAP_TYPE_CGROUP_ARRAY共用同一套 fd 数组实现bpf_fd_array_map_update_elem而HASH_OF_MAPS走bpf_fd_htab_map_update_elem路径见 kernel/bpf/syscall.c。相应地kernel/bpf/syscall.c 中bpf_map_value_size()对外层 map 强制将 value 尺寸取为sizeof(u32)——外层 map 实际存储的是内层 map 的文件描述符u32而不是内层 map 本体这与 sockmap/devmap 等 fd 类 map 的模型一致。两种外层 map 的 key 与内存模型维度BPF_MAP_TYPE_ARRAY_OF_MAPSBPF_MAP_TYPE_HASH_OF_MAPSkey 类型固定的无符号 32 位整数索引定义 map 时可选任意 key 类型/大小容量模型固定大小max_entries个槽位创建时全部零初始化动态分配内核负责 key/value 的分配与回收上限为max_entries内存策略一次性分配默认预分配哈希表元素内存开销过大时可加BPF_F_NO_PREALLOC标志关闭预分配内核实现路径bpf_fd_array_map_update_elem()与 fd 数组共用bpf_fd_htab_map_update_elem()基于 hash table这一分派逻辑在 kernel/bpf/syscall.c 中可以确认} else if (IS_FD_ARRAY(map)) { err bpf_fd_array_map_update_elem(map, map_file, key, value, flags); } else if (map-map_type BPF_MAP_TYPE_HASH_OF_MAPS) { err bpf_fd_htab_map_update_elem(map, map_file, key, value, flags); }生命周期与并发模型inner map 模板与 RCU 同步官方文档强调inner map 实例有独立生命周期创建外层 map 后即可删除。从源码结构看内核在验证阶段基于该模板生成一份inner_map_meta一个仅作验证用途的模板bpf_map结构并关联到外层 mapkernel/bpf/syscall.c 中bpf_map_free()的注释明确说明了这一点/* Note that the btf_record stashed in map-inner_map_meta-record was * already freed using the map_free callback for map in map case which * eventually calls bpf_map_free_meta, since inner_map_meta is only a * template bpf_map struct used during verification. */另一个值得注意的细节是元素替换时的程序可见性。当用户态 update/delete 外层 map 条目换掉某个 fd后正在运行的非睡眠 BPF 程序可能仍持有旧 fd 引用内核通过synchronize_rcu_expedited()等待这些程序退出避免 use-after-fd 窗口static void maybe_wait_bpf_programs(struct bpf_map *map) { /* Wait for any running non-sleepable BPF programs to complete ... */ if (map-map_type BPF_MAP_TYPE_HASH_OF_MAPS || map-map_type BPF_MAP_TYPE_ARRAY_OF_MAPS) synchronize_rcu_expedited(); }见 kernel/bpf/syscall.c。这说明 map-of-maps 的条目更新是准同步操作热路径上应尽量减少外层 map 的改写频率。BPF 程序侧bpf_map_lookup_elem 查询内层 mapBPF 程序中获取内层 map 的接口是标准的bpf_map_lookup_elem()helpervoid *bpf_map_lookup_elem(struct bpf_map *map, const void *key)对外层 map 调用该 helper 时返回值不是 value 缓冲区指针而是内层 map 的struct bpf_map *指针查不到时返回NULL。得到内层 map 指针后即可像操作普通 map 一样对它执行 lookup/update/delete。内核 BPF 示例BTF 声明式初始化外层 map官方文档给出的示例演示了如何用 BTF 声明式declarative方式创建并初始化一个 devmap 数组——外层是ARRAY_OF_MAPS两个槽位分别绑定两个BPF_MAP_TYPE_DEVMAP内层 mapstruct inner_map { __uint(type, BPF_MAP_TYPE_DEVMAP); __uint(max_entries, 10); __type(key, __u32); __type(value, __u32); } inner_map1 SEC(.maps), inner_map2 SEC(.maps); struct { __uint(type, BPF_MAP_TYPE_ARRAY_OF_MAPS); __uint(max_entries, 2); __type(key, __u32); __array(values, struct inner_map); } outer_map SEC(.maps) { .values { inner_map1, inner_map2 } };要点外层 map 用__array(values, struct inner_map)指定内层 map 的类型模板libbpf 加载时会据此创建外层 map 并传入内层 map fd 作为初始化元数据 { .values ... }初始值列表在加载时直接把inner_map1、inner_map2写入对应槽位0 和 1加载完成后外层数组只能在用户态用 syscall API 增删改BPF 程序只读。自测用例剖析三种真实初始化模式内核自测 tools/testing/selftests/bpf/progs/test_btf_map_in_map.c 覆盖了文档未展开的更多细节是研究该特性的最佳一手材料模式一匿名结构体作为内层定义。test_btf_map_in_map.c 中outer_arr直接以匿名 struct 描述内层 map 模板并验证了若模板与初始引用内层 map 的max_entries不一致加载会失败struct outer_arr { __uint(type, BPF_MAP_TYPE_ARRAY_OF_MAPS); __uint(max_entries, 3); __type(key, int); __type(value, int); __array(values, struct { __uint(type, BPF_MAP_TYPE_ARRAY); /* changing max_entries to 2 will fail during load * due to incompatibility with inner_map definition */ __uint(max_entries, 1); __type(key, int); __type(value, int); }); } outer_arr SEC(.maps) { .values { (void *)inner_map1, 0, (void *)inner_map2 }, };初始化列表中允许留空槽位中间的0由于未直接复用struct inner_map类型引用处需要显式(void *)转换。模式二BPF_F_INNER_MAP标志支持动态尺寸内层 map。test_btf_map_in_map.c 中outer_arr_dyn的内层模板带__uint(map_flags, BPF_F_INNER_MAP)。普通模板要求所有内层 map 元数据完全一致而带该标志后允许同一外层 map 容纳max_entries 不同的同类型内层 mapinner_map3/inner_map4容量 3、inner_map5容量 5为运行时按需构造不同规格的内层 map 提供了空间。模式三HASH_OF_MAPS外层 命名初始化。test_btf_map_in_map.c 中outer_hash复用已定义的struct inner_map作为模板用[key] inner的指定初始化语法把内层 map 放到任意 key 位置由于模板类型与引用类型一致编译器可在编译期拦截错误引用。用例末尾的raw_tp/sys_enter程序test_btf_map_in_map.c展示了标准用法先bpf_map_lookup_elem(outer_arr, key)取得struct inner_map *判空后再对内层 map 执行bpf_map_update_elem()inner_map bpf_map_lookup_elem(outer_arr, key); if (!inner_map) return 1; val input; bpf_map_update_elem(inner_map, key, val, 0);用户态使用libbpf 创建外层 map 与填充内层 map用户侧分两步先创建内层模板 map再带着它的 fd 创建外层 map之后用bpf_map_update_elem()把具体内层 map 的 fd 写进外层槽位。官方文档给出的完整代码/* 创建 array 型外层 mapinner_fd 是仅用于元数据的内层模板 map */ int create_outer_array(int inner_fd) { LIBBPF_OPTS(bpf_map_create_opts, opts, .inner_map_fd inner_fd); int fd; fd bpf_map_create(BPF_MAP_TYPE_ARRAY_OF_MAPS, example_array, /* name */ sizeof(__u32), /* key size */ sizeof(__u32), /* value size */ 256, /* max entries */ opts); /* create opts */ return fd; } /* 向外层 map 指定槽位添加一个 devmapvalue 直接传内层 map 的 fd */ int add_devmap(int outer_fd, int index, const char *name) { int fd; fd bpf_map_create(BPF_MAP_TYPE_DEVMAP, name, sizeof(__u32), sizeof(__u32), 256, NULL); if (fd 0) return fd; return bpf_map_update_elem(outer_fd, index, fd, BPF_ANY); }参数说明LIBBPF_OPTS(bpf_map_create_opts, opts, .inner_map_fd inner_fd)通过变参初始化器把模板内层 map 的 fd 传入bpf_map_create()这是内核识别这是 map-of-maps 创建的依据外层 map 的 key/value size 参数只是占位性的内核实际按 u32 fd 处理 value见bpf_map_value_size()max_entries为外层槽位上限bpf_map_update_elem(outer_fd, index, fd, BPF_ANY)中 value 传的是内层 map 的 fd 值BPF_ANY表示不存在则插入、存在则覆盖。实践要点小结一层嵌套 单一内层类型是设计边界规划架构时不要试图套两层内层模板 map 只用于创建时传递元数据创建后无需保留但 BTF 声明式初始化中引用到的内层 map 本身要随程序一起加载BPF 程序对外层 map只读动态替换内层 map 必须走用户态bpf_map_update_elem()/bpf_map_delete_elem()HASH_OF_MAPS内存紧张时加BPF_F_NO_PREALLOCARRAY_OF_MAPS则注意创建即全量零初始化max_entries不宜虚高需要运行时使用不同max_entries的内层 map 时在内层模板上加BPF_F_INNER_MAP见自测用例模式二外层条目替换会触发synchronize_rcu_expedited()属较慢操作避免在高频路径上反复改写外层 map。以上结论均以当前内核源码树中的 Documentation/bpf/map_of_maps.rst、kernel/bpf/syscall.c 与 tools/testing/selftests/bpf/progs/test_btf_map_in_map.c 为依据涉及内核 4.12 引入的版本信息以官方文档标注为准。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考