
Linux BPF 队列与栈 Map 实战BPF_MAP_TYPE_QUEUE 与 BPF_MAP_TYPE_STACK【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文基于 Linux 内核文档 map_queue_stack.rst 讲解BPF_MAP_TYPE_QUEUEFIFO 队列与BPF_MAP_TYPE_STACKLIFO 栈两类 BPF map 的设计与用法包括内核 BPF 侧的三个 helperpush/peek/pop、用户态通过bpf系统调用libbpf 低层 API执行同语义操作的方式并结合 queue_stack_maps.c 的源码解析环形缓冲区结构、标志位校验与错误码语义帮助你在内核与用户态之间安全、高效地传递有序数据。背景与基本概念BPF_MAP_TYPE_QUEUE提供 FIFO先进先出存储BPF_MAP_TYPE_STACK提供 LIFO后进先出存储两者均自内核 4.20 版本引入其类型定义位于 bpf.h。两类 map 均支持 peek、pop 和 push 三种操作peek读取队头/栈顶元素但不移除pop读取并移除队头/栈顶元素push向队列尾部/栈顶压入新元素。这些操作在内核态通过专门的 BPF helper 暴露给 BPF 程序在用户态则复用现有bpf系统调用操作与系统调用命令的对应关系为操作用户态系统调用命令peekBPF_MAP_LOOKUP_ELEMpopBPF_MAP_LOOKUP_AND_DELETE_ELEMpushBPF_MAP_UPDATE_ELEM需要特别注意的是BPF_MAP_TYPE_QUEUE和BPF_MAP_TYPE_STACK不支持BPF_F_NO_PREALLOC标志——元素空间在 map 创建时即一次性完整分配。从源码看这一点在 queue_stack_maps.c 的queue_stack_map_alloc_check()中得到体现创建时的合法标志掩码仅为BPF_F_NUMA_NODE | BPF_F_ACCESS_MASK任何超出该掩码的标志包括BPF_F_NO_PREALLOC都会使创建请求返回-EINVAL。内核态 BPF三个 map helperbpf_map_push_elem()long bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags)通过该 helper 向队列或栈中压入元素value。flags参数必须为BPF_ANY或BPF_EXISTBPF_ANY若 map 已满push 失败并返回-E2BIG见下文源码分析BPF_EXIST若 map 已满则移除最老的元素为新元素腾出空间实现覆盖最老数据的语义。成功返回0失败返回负错误码。bpf_map_peek_elem()long bpf_map_peek_elem(struct bpf_map *map, void *value)从队列或栈中取出一个元素拷贝到value但不移除该元素。成功返回0失败返回负错误码。bpf_map_pop_elem()long bpf_map_pop_elem(struct bpf_map *map, void *value)从队列或栈中移除队头/栈顶元素并拷贝到value。成功返回0失败返回负错误码。这三个 helper 在内核中的实现入口位于 helpers.cBPF_CALL_3(bpf_map_push_elem, struct bpf_map *, map, void *, value, u64, flags) { return map-ops-map_push_elem(map, value, flags); }可以看到 helper 本身只是薄封装真正逻辑通过map-ops虚函数表分发到具体 map 类型的实现。三个 proto 均声明为ARG_CONST_MAP_PTRARG_PTR_TO_MAP_VALUE即第二个参数必须指向 map 中一个 value 大小的内存区域。用户态复用 bpf 系统调用的三种操作用户态程序不需要新系统调用直接复用 libbpf 低层 API但所有调用的key参数必须置为NULL这两类 map 的 key_size 为零pushbpf_map_update_elem()int bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags);将value压入队列或栈。key必须为NULLflags取BPF_ANY或BPF_EXIST语义与内核 helperbpf_map_push_elem完全一致。返回0表示成功。peekbpf_map_lookup_elem()int bpf_map_lookup_elem(int fd, const void *key, void *value);窥看队列或栈头部的value。key必须为NULL。返回0表示成功。popbpf_map_lookup_and_delete_elem()int bpf_map_lookup_and_delete_elem(int fd, const void *key, void *value);从队列或栈头部弹出value。key必须为NULL。返回0表示成功。这套复用旧命令的做法在 syscall.c 中可以直接看到bpf_map_update_elem系统调用处理函数识别到 map 类型为BPF_MAP_TYPE_QUEUE、BPF_MAP_TYPE_STACK或BPF_MAP_TYPE_BLOOM_FILTER时绕开常规的 update 路径改走map-ops-map_push_elem(map, value, flags)BPF_MAP_LOOKUP_ELEM路径同理改调map_peek_elem见 syscall.cBPF_MAP_LOOKUP_AND_DELETE_ELEM路径则改调map_pop_elem见 syscall.c。也就是说同一个系统调用命令在这两类 map 上被赋予了队列/栈语义而非通用哈希/数组语义。源码深潜queue_stack_maps.c 的实现细节两类 map 共享同一个实现文件 queue_stack_maps.c差异仅在于 pop/peek 取的是哪一端。数据结构预分配的环形缓冲区struct bpf_queue_stack { struct bpf_map map; rqspinlock_t lock; u32 head, tail; u32 size; /* max_entries 1 */ char elements[] __aligned(8); };底层是一个size max_entries 1的环形缓冲区elements柔性数组head指向下一个写入位置tail指向下一个读取位置之所以比max_entries多分配一个槽位是为了用head tail判空queue_stack_map_is_empty()、head1 tail判满queue_stack_map_is_full()从而无需额外维护计数器并发控制使用rqspinlockraw_res_spin_lock_irqsave保证内核 BPF 程序与用户态系统调用可以并发安全地操作同一个 map内存通过bpf_map_area_alloc()按 NUMA 节点一次性分配与不支持BPF_F_NO_PREALLOC的文档描述相互印证。创建时的参数校验queue_stack_map_alloc_check()强制要求queue_stack_maps.cmax_entries必须大于 0key_size必须为 0——这正是用户态所有调用中key必须传NULL的原因value_size必须大于 0且不得超过KMALLOC_MAX_SIZE否则用户态无法访问元素返回-E2BIG允许的标志仅为BPF_F_NUMA_NODE | BPF_F_ACCESS_MASK。push 路径的完整语义queue_stack_map_push_elem()queue_stack_maps.c的关键逻辑bool replace (flags BPF_EXIST); /* Check supported flags for queue and stack maps */ if (flags BPF_NOEXIST || flags BPF_EXIST) return -EINVAL; ... if (queue_stack_map_is_full(qs)) { if (!replace) { err -E2BIG; goto out; } /* advance tail pointer to overwrite oldest element */ if (unlikely(qs-tail qs-size)) qs-tail 0; }由此可以总结出完整的标志位与错误码语义表场景结果flags含BPF_NOEXIST或值超过BPF_EXIST-EINVALmap 已满且flags BPF_ANY-E2BIGmap 已满且flags BPF_EXIST前移tail覆盖最老元素后写入成功普通写入拷贝value_size字节到head位置head环形递增锁被抢占竞争raw_res_spin_lock_irqsave失败-EBUSY且value缓冲区被清零值得注意的是 peek/pop 路径__queue_map_get()/__stack_map_get()在 map 为空时会把value缓冲区清零并返回-ENOENT而 queue 的 pop/peek 取tailFIFO 头stack 的 pop/peek 取head - 1LIFO 顶这就是两类 map 唯一的行为差异。被刻意禁用的操作实现中还定义了一组必败的 stub 操作static void *queue_stack_map_lookup_elem(struct bpf_map *map, void *key) { return NULL; } static long queue_stack_map_update_elem(struct bpf_map *map, void *key, void *value, u64 flags) { return -EINVAL; }map_lookup_elem、map_update_elem、map_delete_elem、map_get_next_key均返回NULL或-EINVAL——队列/栈天然没有按 key 随机寻址和遍历 key的概念。这解释了为什么内核态不能用bpf_map_lookup_elem()helper 按 key 访问这两类 map也不能用BPF_MAP_GET_NEXT_KEY枚举元素唯一合法的访问路径就是 push/peek/pop。两个 map 类型的操作表queue_map_ops与stack_map_opsqueue_stack_maps.c共享 alloc/free/check 与 push 实现仅在map_pop_elem与map_peek_elem两个槽位上分别指向queue_map_pop_elem/queue_map_peek_elem与stack_map_pop_elem/stack_map_peek_elem。使用示例内核 BPF 程序声明一个队列 mapstruct { __uint(type, BPF_MAP_TYPE_QUEUE); __type(value, __u32); __uint(max_entries, 10); } queue SEC(.maps);注意声明中没有key类型——与key_size 必须为零的内核约束一致。用户态用 libbpf 低层 API 创建队列int create_queue() { return bpf_map_create(BPF_MAP_TYPE_QUEUE, sample_queue, /* name */ 0, /* key size, must be zero */ sizeof(__u32), /* value size */ 10, /* max entries */ NULL); /* create options */ }参数要点key_size必须传0否则内核侧queue_stack_map_alloc_check()会返回-EINVALvalue_size决定每个元素的字节大小示例为 4 字节的__u32且不能大于KMALLOC_MAX_SIZEmax_entries即最大元素个数实际预分配空间为(max_entries 1) * value_size加上结构体头部与源码中queue_stack_map_mem_usage()的计算方式一致最后的create options可传BPF_F_NUMA_NODE相关的 NUMA 选项或访问控制选项其余标志均不被接受。创建后即可获得 fd后续用前文所述的bpf_map_update_elem/bpf_map_lookup_elem/bpf_map_lookup_and_delete_elemkey均为NULL完成 push/peek/pop。典型应用场景小结结合文档描述与源码语义这两类 map 适合内核态事件缓冲在 XDP、TC、tracepoint 等程序中把事件压入队列用户态消费者程序循环bpf_map_lookup_and_delete_elem批量拉取形成低开销的生产者-消费者管道溢出丢弃策略高负载下用BPF_EXIST标志 push让最老数据被自动覆盖保证消费者始终看到最新数据LIFO 重试/回退栈用BPF_MAP_TYPE_STACK记录调用层次或待回退状态后发生先处理。使用时需牢记的前提与限制内核 4.20key 恒为 0不支持BPF_F_NO_PREALLOC不能按 key 查、不能删除中间元素、不能遍历 key满时BPF_ANYpush 返回-E2BIG空时 peek/pop 返回-ENOENT且value缓冲会被清零无需额外初始化判断。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考