
Linux 内核 BPF 数组存储深入解析BPF_MAP_TYPE_ARRAY 与 BPF_MAP_TYPE_PERCPU_ARRAY 实战【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文以内核文档 Documentation/bpf/map_array.rst 为核心系统讲解 Linux 内核中BPF_MAP_TYPE_ARRAY与BPF_MAP_TYPE_PERCPU_ARRAY两类数组型 BPF map 的数据结构、创建约束、内核侧 helper 调用、用户态 libbpf 操作以及并发控制机制并结合 kernel/bpf/arraymap.c 的实现源码帮助读者建立从 API 用法到内核底层内存布局的完整认知。读完后可掌握如何在内核 BPF 程序与用户态程序中正确声明、初始化、查询这两类 map如何启用BPF_F_MMAPABLE做内存映射访问以及 per-CPU 变体在多核环境下的语义差异与陷阱如BPF_NOEXIST不可用、删除操作被拒绝等。一、两类 map 的定位与共同约束BPF_MAP_TYPE_ARRAY与BPF_MAP_TYPE_PERCPU_ARRAY提供通用的数组存储两者的关键事实如下引入版本BPF_MAP_TYPE_ARRAY自 3.19 引入BPF_MAP_TYPE_PERCPU_ARRAY自 4.6 引入key 类型无符号 32 位整数4 字节即u32作为数组下标恒定容量map 在创建时通过max_entries定义大小此后不可变所有元素在创建时即预分配并零初始化value 类型BPF_MAP_TYPE_ARRAY的 value 大小任意BPF_MAP_TYPE_PERCPU_ARRAY的 value 不超过PCPU_MIN_UNIT_SIZE32 KiB内存模型差异BPF_MAP_TYPE_ARRAY所有 CPU 共享同一块内存区域BPF_MAP_TYPE_PERCPU_ARRAY为每个 CPU 分配独立的内存区域对齐所有数组元素按 8 字节对齐。这些约束在内核源码中有一一对应的实现。include/linux/percpu.h第 25 行定义了该上限#define PCPU_MIN_UNIT_SIZE PFN_ALIGN(32 10)而 kernel/bpf/arraymap.c 中的array_map_alloc_check()是所有创建请求的合法性闸门int array_map_alloc_check(union bpf_attr *attr) { bool percpu attr-map_type BPF_MAP_TYPE_PERCPU_ARRAY; int numa_node bpf_map_attr_numa_node(attr); /* check sanity of attributes */ if (attr-max_entries 0 || attr-key_size ! 4 || attr-value_size 0 || attr-map_flags ~ARRAY_CREATE_FLAG_MASK || !bpf_map_flags_access_ok(attr-map_flags) || (percpu numa_node ! NUMA_NO_NODE)) return -EINVAL; ... /* percpu map value size is bound by PCPU_MIN_UNIT_SIZE */ if (percpu round_up(attr-value_size, 8) PCPU_MIN_UNIT_SIZE) return -E2BIG; return 0; }从源码结构看可以确认几点文档未显式展开的硬性限制max_entries为 0、key_size ! 4、value_size为 0 均直接返回-EINVAL允许的创建标志被ARRAY_CREATE_FLAG_MASK限定为BPF_F_NUMA_NODE | BPF_F_MMAPABLE | BPF_F_ACCESS_MASK | BPF_F_PRESERVE_ELEMS | BPF_F_INNER_MAP其他标志位一律拒绝BPF_F_MMAPABLE与BPF_F_INNER_MAP只允许用于BPF_MAP_TYPE_ARRAYper-CPU 变体不支持源码第 66-68 行per-CPU 变体不允许指定 NUMA 节点percpu numa_node ! NUMA_NO_NODE时报错per-CPU 的 value 大小按 8 字节取整后若超过 32 KiB返回-E2BIG——这就是文档中PCPU_MIN_UNIT_SIZE上限的落地点。BPF_F_MMAPABLE用户态内存映射访问自 5.5 版内核起BPF_MAP_TYPE_ARRAY可通过设置BPF_F_MMAPABLE标志定义见 include/uapi/linux/bpf.hBPF_F_MMAPABLE (1U 10)开启内存映射。此时内存布局为map 定义结构struct bpf_array页对齐占据第一页从第二页起分配足够存放所有数组值的页对齐内存块。由于按页对齐分配某些场景下会产生超额分配over-allocation好处是用户态程序无需再逐个调用 helper/系统调用读写数据直接映射进进程地址空间即可访问性能与易用性同时提升。实现见 kernel/bpf/arraymap.c 的array_map_alloc()if (attr-map_flags BPF_F_MMAPABLE) { array_size PAGE_ALIGN(array_size); array_size PAGE_ALIGN((u64) max_entries * elem_size); } ... if (attr-map_flags BPF_F_MMAPABLE) { void *data; /* kmalloced memory cant be mmaped, use explicit vmalloc */ data bpf_map_area_mmapable_alloc(array_size, numa_node); if (!data) return ERR_PTR(-ENOMEM); array data PAGE_ALIGN(sizeof(struct bpf_array)) - offsetof(struct bpf_array, value); }可以看到 mmapable map 强制走 vmallockmalloc 的内存无法被 mmap且array指针被调整到“第一页结构区 第二页起始”的位置确保array-value恰好页对齐。mmap 时的访问路径由array_map_mmap()与按需缺页处理的array_map_mmap_fault()实现kernel/bpf/arraymap.c后者通过vmalloc_to_page()将缺页偏移映射到实际物理页且对越界偏移vm_pgoff超出max_entries * elem_size范围直接返回-EINVAL。二、内核 BPF 侧helper 函数与行为语义bpf_map_lookup_elem()void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)用该 helper 取回数组元素。注意它返回的是指向数组元素内部的指针而非副本因此若用户态程序可能同时读取该 valueBPF 程序原地更新时必须使用__sync_fetch_and_add()之类的原子原语避免数据竞争。内核实现array_map_lookup_elem()kernel/bpf/arraymap.c只有两步下标越界检查index max_entries返回NULL然后array-value elem_size * (index index_mask)直接寻址——由于元素全部预分配不存在“键不存在”的正常语义越界是唯一失败路径。还有一个值得注意的优化array_map_gen_lookup()kernel/bpf/arraymap.c会在加载阶段把对数组 map 的bpf_map_lookup_elem()调用内联为一条 BPF 指令序列取 map 基址、边界检查、按位与index_mask、按elem_size移位或乘数寻址完全跳过函数调用开销。其中的index index_mask将下标钳制到 2 的幂掩码内是 Spectre v1 的推测执行防护数组容量在分配时被向上取整到 2 的幂见array_map_alloc()第 106-114 行CPU 对越界下标的推测访问只会落在合法预分配区域内。bpf_map_update_elem()long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, u64 flags)用于更新数组元素成功返回 0失败返回负错误码。实现array_map_update_elem()kernel/bpf/arraymap.c的语义值得逐条对照未知标志超出BPF_EXIST且不含BPF_F_LOCK返回-EINVAL下标超出max_entries返回-E2BIG注释明确“all elements were pre-allocated, cannot insert a new one”BPF_NOEXIST标志返回-EEXIST——因为所有元素在创建时已存在普通数组路径用copy_map_value()拷贝值per-CPU 路径通过this_cpu_ptr()只写当前 CPU 的槽位带BPF_F_LOCK时走copy_map_value_locked()且要求 map 的 BTF 记录中确实包含bpf_spin_lock字段否则-EINVAL。为什么不能删除元素文档明确指出数组是恒定容量bpf_map_delete_elem()不被支持。源码中这一点以最直接的方式体现——array_map_ops与percpu_array_map_ops挂载的删除实现kernel/bpf/arraymap.c恒返回-EINVALstatic long array_map_delete_elem(struct bpf_map *map, void *key) { return -EINVAL; }因此“清空”某个元素的正确做法是用bpf_map_update_elem()向该下标写入零值。三、Per-CPU 数组隔离存储与跨 CPU 查询为什么需要 per-CPU 变体BPF_MAP_TYPE_ARRAY中存储的 value 会被不同 CPU 上的多个 BPF 程序并发访问高频计数器场景下会产生缓存行争用。BPF_MAP_TYPE_PERCPU_ARRAY将存储限制在单 CPU每个下标在每个 CPU 上各有一份副本。分配逻辑在bpf_array_alloc_percpu()kernel/bpf/arraymap.c为每个下标调用一次bpf_map_alloc_percpu()取得void __percpu *存入array-pptrs[i]失败则整体回滚释放。内存占用可从array_map_mem_usage()kernel/bpf/arraymap.c读出per-CPU 变体的用量约为max_entries * elem_size * num_possible_cpus()即总内存是普通数组的 CPU 数倍这是使用 per-CPU 变体时必须权衡的成本。自动路由到当前 CPU使用BPF_MAP_TYPE_PERCPU_ARRAY时bpf_map_update_elem()与bpf_map_lookup_elem()两个 helper 会自动访问当前 CPU 的槽位。查找实现percpu_array_map_lookup_elem()kernel/bpf/arraymap.cstatic void *percpu_array_map_lookup_elem(struct bpf_map *map, void *key) { struct bpf_array *array container_of(map, struct bpf_array, map); u32 index *(u32 *)key; if (unlikely(index array-map.max_entries)) return NULL; return this_cpu_ptr(array-pptrs[index array-index_mask]); }this_cpu_ptr()保证拿到的永远是运行 CPU 本地的那份数据BPF 程序无需任何额外代码即可做到无锁的每核计数/统计——这正是 per-CPU 变体在 XDP、tc 等高频路径上的核心卖点。bpf_map_lookup_percpu_elem()void *bpf_map_lookup_percpu_elem(struct bpf_map *map, const void *key, u32 cpu)该 helper 用于查询指定 CPU上的数组值成功返回指针找不到条目或cpu无效时返回NULL。实现percpu_array_map_lookup_percpu_elem()kernel/bpf/arraymap.c的判序是先检查cpu nr_cpu_ids返回NULL再检查下标越界最后per_cpu_ptr(array-pptrs[index index_mask], cpu)取目标 CPU 的副本。helper 的分发逻辑在 kernel/bpf/helpers.cBPF_CALL_3(bpf_map_lookup_percpu_elem, struct bpf_map *, map, void *, key, u32, cpu) { ... return (unsigned long) map-ops-map_lookup_percpu_elem(map, key, cpu); }注意它只对实现了map_lookup_percpu_elem的 map 类型per-CPU 数组/哈希等有效verifier 也会在 kernel/bpf/verifier.c 中按 prog 类型约束该 helper 的可用性。四、并发控制自 5.1 版内核起BPF 基础设施提供struct bpf_spin_lock用于同步访问UAPI 定义见 include/uapi/linux/bpf.h。在数组 map 中其用法有两条路径BPF 程序内对带bpf_spin_lock字段的 value用bpf_spin_lock()/bpf_spin_unlock()helper 保护临界区BPF_F_LOCK语义对应BPF_F_LOCK 4见 include/uapi/linux/bpf.h带锁更新bpf_map_update_elem()传BPF_F_LOCK时内核侧用copy_map_value_locked()在持有 map 值中自旋锁的状态下完成拷贝见array_map_update_elem()第 411-413 行对 BTF 字段的校验防止对未声明锁的 map 使用锁标志。而对于简单计数场景文档示例采用的__sync_fetch_and_add()原子操作是更轻量的选择——per-CPU 变体则从根源上消除了跨 CPU 竞争两者可组合使用。五、完整示例功能级示例可参考 tools/testing/selftests/bpf 目录其中包含 tools/testing/selftests/bpf/prog_tests/map_lookup_percpu_elem.c、tools/testing/selftests/bpf/prog_tests/mmap.c、tools/testing/selftests/bpf/map_tests/array_map_batch_ops.c 等针对数组 map 的测试。以下代码样例展示 API 用法。内核 BPF 侧声明一个数组 mapstruct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, u32); __type(value, long); __uint(max_entries, 256); } my_map SEC(.maps);一个访问数组元素的示例 BPF 程序——按 IP 协议号做报文长度累加统计int bpf_prog(struct __sk_buff *skb) { struct iphdr ip; int index; long *value; if (bpf_skb_load_bytes(skb, ETH_HLEN, ip, sizeof(ip)) 0) return 0; index ip.protocol; value bpf_map_lookup_elem(my_map, index); if (value) __sync_fetch_and_add(value, skb-len); return 0; }注意这里用__sync_fetch_and_add()做原地累加因为 lookup 返回的是内核共享内存中的指针若用户态程序同时读取该值普通读写会造成数据竞争。用户态BPF_MAP_TYPE_ARRAY使用bpf_map_create_opts设置BPF_F_MMAPABLE标志创建数组#include bpf/libbpf.h #include bpf/bpf.h int create_array() { int fd; LIBBPF_OPTS(bpf_map_create_opts, opts, .map_flags BPF_F_MMAPABLE); fd bpf_map_create(BPF_MAP_TYPE_ARRAY, example_array, /* name */ sizeof(__u32), /* key size */ sizeof(long), /* value size */ 256, /* max entries */ opts); /* create opts */ return fd; }初始化数组元素注意元素创建时已零初始化这里只是显式写入初值int initialize_array(int fd) { __u32 i; long value; int ret; for (i 0; i 256; i) { value i; ret bpf_map_update_elem(fd, i, value, BPF_ANY); if (ret 0) return ret; } return ret; }读取某个元素int lookup(int fd) { __u32 index 42; long value; int ret; ret bpf_map_lookup_elem(fd, index, value); if (ret 0) return ret; /* use value here */ assert(value 42); return ret; }用户态 API 与内核 helper 同名区别仅在于以 map 的fd标识 map且参数为指针时内核会做copy_map_value()式拷贝用户态拿到的是副本而非共享指针因此不存在文档示例中__sync那类原地修改的竞争问题。用户态BPF_MAP_TYPE_PERCPU_ARRAYper-CPU 数组的用户态值布局是一个含ncpus个元素的缓冲区更新与查询都要按此组织数据。初始化int initialize_array(int fd) { int ncpus libbpf_num_possible_cpus(); long values[ncpus]; __u32 i, j; int ret; for (i 0; i 256; i) { for (j 0; j ncpus; j) values[j] i; ret bpf_map_update_elem(fd, i, values, BPF_ANY); if (ret 0) return ret; } return ret; }读取各 CPU 的值int lookup(int fd) { int ncpus libbpf_num_possible_cpus(); __u32 index 42, j; long values[ncpus]; int ret; ret bpf_map_lookup_elem(fd, index, values); if (ret 0) return ret; for (j 0; j ncpus; j) { /* Use per CPU value here */ assert(values[j] 42); } return ret; }从源码看这条“ncpus 个元素”的布局不是约定而是实现bpf_percpu_array_copy()kernel/bpf/arraymap.c在用户态 lookup 时执行for_each_possible_cpu(cpu)循环把每个可能 CPU 的副本依次拷贝到调用方缓冲区每份占elem_size即round_up(value_size, 8)字节若 flags 带BPF_F_CPU则只拷贝指定 CPUCPU 号放在map_flags 32。更新侧的bpf_percpu_array_update()kernel/bpf/arraymap.c同理默认把调用方提供的整块 ncpus 缓冲区逐 CPU 拆开放置带BPF_F_ALL_CPUS标志时则把同一个值复制到所有 CPU。六、语义细节与边界行为per-CPU 值的用户态布局如第五节所示访问BPF_MAP_TYPE_PERCPU_ARRAY时每个 value 是一个含ncpus个元素的数组聚合时需要用户态自行求和。内核侧 seq_file 打印percpu_array_map_seq_show_elem()kernel/bpf/arraymap.c同样按cpu%d: ...逐 CPU 展示与上述布局一致。BPF_NOEXIST不可用对这两类 map 调用bpf_map_update_elem()时传入BPF_NOEXIST必然失败普通数组返回-EEXIST见array_map_update_elem()第 407-409 行per-CPU 路径见bpf_percpu_array_update()第 449-451 行因为所有槽位在创建时已存在。bpf_map_delete_elem()不被支持如前所述实现恒返回-EINVAL清空元素请写入零值。枚举与批量操作bpf_array_get_next_key()kernel/bpf/arraymap.c按“下标 1”语义返回下一个 key首 key 为 0末位后返回-ENOENTops 表同时挂载了generic_map_lookup_batch/generic_map_update_batch即支持 BPF_MAP_GET_NEXT_KEY 与 batch 系统调用批量操作的行为可由 tools/testing/selftests/bpf/map_tests/array_map_batch_ops.c 验证。创建参数陷阱速查均出自array_map_alloc_check()/array_map_update_elem()key 必须 4 字节、value_size 必须非 0、per-CPU value ≤ 32 KiB对齐后、per-CPU 不可加BPF_F_MMAPABLE/BPF_F_INNER_MAP/NUMA 节点、更新越界返回-E2BIG、未知标志返回-EINVAL。七、选型建议与适用前提多核高频计数、无锁统计选BPF_MAP_TYPE_PERCPU_ARRAY。每个 BPF 程序只写本 CPU 槽位无争用、无锁代价是内存为普通数组的 ncpus 倍且用户态读取时要跨 CPU 聚合。需要用户态高频读写的共享数据如配置表、阈值表选BPF_MAP_TYPE_ARRAY并加BPF_F_MMAPABLE要求 5.5 内核映射后可直接用指针读写省去系统调用注意 map 结构区占第一页、value 区自第二页起的布局与页对齐带来的超额分配。简单共享计数、可容忍原子操作普通BPF_MAP_TYPE_ARRAY配合__sync_fetch_and_add()等原子原语即可内存最省。并发临界区value 内含多字段需整体一致性修改在 value 结构中加入struct bpf_spin_lock并使用BPF_F_LOCK或bpf_spin_lock()helper5.1 内核。以上行为均以当前内核树中 kernel/bpf/arraymap.c 与 include/uapi/linux/bpf.h 的实际实现为准文档中提到的“3.19/4.6/5.1/5.5”版本边界是各特性的引入下限在新内核上使用时这些语义仍然成立。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考