Linux BPF 队列与栈 Map 实战:BPF_MAP_TYPE_QUEUE 与 BPF_MAP_TYPE_STACK
2026/9/14 6:42:44 网站建设 项目流程

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_QUEUE(FIFO 队列)与BPF_MAP_TYPE_STACK(LIFO 栈)两类 BPF map 的设计与用法:包括内核 BPF 侧的三个 helper(push/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_ELEM
popBPF_MAP_LOOKUP_AND_DELETE_ELEM
pushBPF_MAP_UPDATE_ELEM

需要特别注意的是:BPF_MAP_TYPE_QUEUEBPF_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 helper

bpf_map_push_elem()

long bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags)

通过该 helper 向队列或栈中压入元素valueflags参数必须为BPF_ANYBPF_EXIST

  • BPF_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.c:

BPF_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_PTR+ARG_PTR_TO_MAP_VALUE,即第二个参数必须指向 map 中一个 value 大小的内存区域。

用户态:复用 bpf 系统调用的三种操作

用户态程序不需要新系统调用,直接复用 libbpf 低层 API,但所有调用的key参数必须置为NULL(这两类 map 的 key_size 为零):

push:bpf_map_update_elem()

int bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags);

value压入队列或栈。key必须为NULLflagsBPF_ANYBPF_EXIST,语义与内核 helperbpf_map_push_elem完全一致。返回0表示成功。

peek:bpf_map_lookup_elem()

int bpf_map_lookup_elem(int fd, const void *key, void *value);

窥看队列或栈头部valuekey必须为NULL。返回0表示成功。

pop:bpf_map_lookup_and_delete_elem()

int bpf_map_lookup_and_delete_elem(int fd, const void *key, void *value);

从队列或栈头部弹出valuekey必须为NULL。返回0表示成功。

这套"复用旧命令"的做法在 syscall.c 中可以直接看到:bpf_map_update_elem系统调用处理函数识别到 map 类型为BPF_MAP_TYPE_QUEUEBPF_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.c),BPF_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())、"head+1 == 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.c):

  • max_entries必须大于 0;
  • key_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; }

由此可以总结出完整的标志位与错误码语义表:

场景结果
flagsBPF_NOEXIST,或值超过BPF_EXIST-EINVAL
map 已满且flags = BPF_ANY-E2BIG
map 已满且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 取tail(FIFO 头),stack 的 pop/peek 取head - 1(LIFO 顶),这就是两类 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_elemmap_update_elemmap_delete_elemmap_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_opsstack_map_ops(queue_stack_maps.c)共享 alloc/free/check 与 push 实现,仅在map_pop_elemmap_peek_elem两个槽位上分别指向queue_map_pop_elem/queue_map_peek_elemstack_map_pop_elem/stack_map_peek_elem

使用示例

内核 BPF 程序:声明一个队列 map

struct { __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()会返回-EINVAL
  • value_size决定每个元素的字节大小(示例为 4 字节的__u32),且不能大于KMALLOC_MAX_SIZE
  • max_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 适合:

  1. 内核态事件缓冲:在 XDP、TC、tracepoint 等程序中把事件压入队列,用户态消费者程序循环bpf_map_lookup_and_delete_elem批量拉取,形成低开销的生产者-消费者管道;
  2. 溢出丢弃策略:高负载下用BPF_EXIST标志 push,让最老数据被自动覆盖,保证消费者始终看到最新数据;
  3. LIFO 重试/回退栈:用BPF_MAP_TYPE_STACK记录调用层次或待回退状态,后发生先处理。

使用时需牢记的前提与限制:内核 4.20+;key 恒为 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询