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系统调用,操作与系统调用命令的对应关系为:
| 操作 | 用户态系统调用命令 |
|---|---|
| peek | BPF_MAP_LOOKUP_ELEM |
| pop | BPF_MAP_LOOKUP_AND_DELETE_ELEM |
| push | BPF_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 helper
bpf_map_push_elem()
long bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags)通过该 helper 向队列或栈中压入元素value。flags参数必须为BPF_ANY或BPF_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必须为NULL,flags取BPF_ANY或BPF_EXIST,语义与内核 helperbpf_map_push_elem完全一致。返回0表示成功。
peek:bpf_map_lookup_elem()
int bpf_map_lookup_elem(int fd, const void *key, void *value);窥看队列或栈头部的value。key必须为NULL。返回0表示成功。
pop:bpf_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.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()),从而无需额外维护计数器; - 并发控制使用
rqspinlock(raw_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; }由此可以总结出完整的标志位与错误码语义表:
| 场景 | 结果 |
|---|---|
flags含BPF_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_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_ops(queue_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 程序:声明一个队列 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_elem(key均为NULL)完成 push/peek/pop。
典型应用场景小结
结合文档描述与源码语义,这两类 map 适合:
- 内核态事件缓冲:在 XDP、TC、tracepoint 等程序中把事件压入队列,用户态消费者程序循环
bpf_map_lookup_and_delete_elem批量拉取,形成低开销的生产者-消费者管道; - 溢出丢弃策略:高负载下用
BPF_EXIST标志 push,让最老数据被自动覆盖,保证消费者始终看到最新数据; - 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),仅供参考