BPF
程序在内核中执行时,不能访问全局变量,不能调用任意内核函数,也不能使用
malloc()。BPF
程序唯一能持久化状态、与用户态交换数据的机制就是 BPF
map。这不是一个简单的 key-value store 包装——每种 map
类型背后是一套独立的数据结构和并发策略,选错类型带来的性能退化可能是数量级的。
本文是第一篇拆解 BPF map 内核实现的文章。我们从
struct bpf_map 和 bpf_map_ops
虚函数表出发,逐层剖析 BPF_MAP_TYPE_HASH(hash
表)、BPF_MAP_TYPE_ARRAY(数组)以及它们的
per-CPU 变体的内核实现、并发模型和适用场景。
源码基准:Linux 6.8,路径
kernel/bpf/hashtab.c、kernel/bpf/arraymap.c、include/linux/bpf.h。
一、问题的起点:一个可编程子系统的存储需求
考虑一个最简单的需求:写一个 BPF 程序统计每个 CPU 上的网络包数量。你需要:
- 一块内存来存计数器(BPF 不能
malloc) - 多核并发写这 64 个计数器时不加锁(同一 CPU 同时只有一个 BPF 程序在跑,但不同 CPU 会并发写同一块内存)
- 用户态程序能读到所有 CPU 的汇总值
通用 key-value store
做不到第三点,锁保护的全局数组做不到第二点。BPF map 的
per-CPU array 用 this_cpu_ptr()
解决了这个问题——每个 CPU
一份私有拷贝、无锁读写、用户态汇总。这不是一个”BPF 版
hashmap”的故事,这是”内核为 BPF
的执行约束专门设计了存储子系统”的故事。
在深入具体实现之前,先建立 BPF map 子系统的骨架。
二、BPF Map 子系统的骨架:bpf_map 与 bpf_map_ops
2.1 struct bpf_map:一张 map 的内核表示
所有 map 类型共享同一个头结构
struct bpf_map,定义在
include/linux/bpf.h:
/* include/linux/bpf.h — Linux 6.8 */
struct bpf_map {
/* map 元数据 */
const struct bpf_map_ops *ops; /* 虚函数表:每种类型的操作实现 */
struct bpf_map *inner_map_meta; /* map-in-map 的内层 map 模板 */
void *record; /* struct_ops 使用 */
/* 用户设置的属性 */
u32 map_type; /* BPF_MAP_TYPE_HASH / ARRAY / ... */
u32 key_size;
u32 value_size;
u32 max_entries;
u64 map_extra; /* 类型相关的额外标志 */
u32 map_flags; /* BPF_F_NO_PREALLOC / BPF_F_RDONLY_PROG 等 */
u32 id; /* 内核分配的唯一 ID */
int numa_node;
u32 btf_key_type_id;
u32 btf_value_type_id;
/* 内存统计 */
u32 pages; /* 占用的页面数 */
bool bypass_spec_v1; /* Spectre V1 缓解 */
bool frozen; /* 冻结(禁止修改) */
/* 引用计数与生命周期 */
atomic64_t refcnt; /* 用户态 FD 引用 */
atomic64_t usercnt; /* 用户态持有计数 */
struct work_struct work; /* RCU 延迟释放 */
struct mutex freeze_mutex;
atomic64_t writecnt; /* 写入者计数(map-in-map 使用) */
/* 所有者信息 */
char name[BPF_OBJ_NAME_LEN];
struct bpf_map_off_arr *off_arr;
...
};关键字段解读:
map_type决定ops指向哪个实现——每个BPF_MAP_TYPE_*枚举值对应一个bpf_map_ops实例。key_size和value_size在 map 创建时固定,运行时不可变。max_entries是最大元素数。对于 hash map,这决定 bucket 数量(向上取整的 2 的幂)。对于 array map,这就是数组长度。map_flags携带类型相关的行为标志,例如BPF_F_NO_PREALLOC控制 hash map 是否预分配元素内存。
2.2 struct bpf_map_ops:虚函数表
每种 map 类型通过 bpf_map_ops
提供自己的操作实现。定义在
include/linux/bpf.h:
/* include/linux/bpf.h — Linux 6.8 */
struct bpf_map_ops {
/* 生命周期 */
int (*map_alloc_check)(union bpf_attr *attr);
struct bpf_map *(*map_alloc)(union bpf_attr *attr);
void (*map_free)(struct bpf_map *map);
int (*map_extra_elem_count)(const struct bpf_map *map);
/* 核心 CRUD */
void *(*map_lookup_elem)(struct bpf_map *map, void *key);
long (*map_update_elem)(struct bpf_map *map, void *key,
void *value, u64 flags);
long (*map_delete_elem)(struct bpf_map *map, void *key);
/* 遍历 */
int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key);
/* 内存映射(array map 的关键操作) */
int (*map_mmap)(struct bpf_map *map, struct vm_area_struct *vma);
/* syscall 级别操作 */
int (*map_lookup_elem_sys_only)(struct bpf_map *map, void *key, void *value);
int (*map_update_elem_sys_only)(struct bpf_map *map, void *key,
void *value, u64 flags);
/* map-in-map 支持 */
void *(*map_fd_sys_lookup_elem)(void *ptr);
void *(*map_fd_sys_lookup_elem_sys_only)(void *ptr);
/* 批量操作 */
int (*map_lookup_batch)(struct bpf_map *map, u64 *total_count,
u64 *batch, void *keys, void *values,
u32 *count, void *elem_flags,
bool is_lookup_batch, bool has_uidx_opt);
int (*map_update_batch)(struct bpf_map *map, u64 *total_count,
u64 *batch, void *keys, void *values,
u32 *count, void *elem_flags,
bool is_lookup_batch, bool has_uidx_opt);
int (*map_delete_batch)(struct bpf_map *map, u64 *total_count,
u64 *batch, void *keys, void *values,
u32 *count, void *elem_flags,
bool is_lookup_batch, bool has_uidx_opt);
/* BPF 程序端操作(不同于 syscall 端) */
void *(*map_lookup_percpu_elem)(struct bpf_map *map, void *key, u32 cpu);
/* 更多特殊操作 */
int (*map_set_for_each_callback_args)(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
struct bpf_func_state *callee);
...
};这个虚函数表的规模说明了一件事:BPF map
不是一个简单的”内核版 unordered_map”。不同 map
类型在这些操作上的实现差异巨大——array map 的
map_lookup_elem 是 O(1) 数组索引,而 hash map
的是 O(1) 哈希查找。array map 不支持
map_delete_elem(返回错误),而 hash map
支持。
理解了这个骨架之后,我们进入第一个具体实现:hash map。
三、BPF_MAP_TYPE_HASH:htab 的分桶链表与预分配策略
Hash map(BPF_MAP_TYPE_HASH)是使用最广泛的
BPF map 类型。内核实现位于
kernel/bpf/hashtab.c。核心数据结构是
struct bpf_htab:
/* kernel/bpf/hashtab.c — Linux 6.8 */
struct bpf_htab {
struct bpf_map map; /* 嵌入基类 */
struct bpf_mem_alloc ma; /* 元素内存分配器 */
struct bucket *buckets; /* bucket 数组 */
void *elems; /* prealloc 模式下的元素存储 */
union {
struct pcpu_htab_value __percpu *pprev; /* per-CPU 变体使用 */
...
};
atomic_t count; /* 当前元素数 */
u32 n_buckets; /* bucket 数量(2 的幂) */
u32 elem_size; /* htab_elem + key + value 的总大小 */
...
};3.1 Bucket 与链表:拉链法(Separate Chaining)
struct bucket 本身就是单链表头节点:
/* kernel/bpf/hashtab.c — Linux 6.8 */
struct bucket {
struct hlist_raw_head head; /* 实际是 hlist_nulls_head */
raw_spinlock_t raw_lock; /* 每 bucket 一把自旋锁 */
};
struct htab_elem {
union {
struct hlist_nulls_node hash_node; /* 链表节点(查找时用) */
struct {
void *padding;
union {
struct pcpu_htab_value __percpu *pprev; /* per-CPU 反向指针 */
...
};
};
};
union {
struct rcu_head rcu; /* RCU 延迟释放 */
enum {
HTAB_ELEM_EXTRA_ENT,
HTAB_ELEM_PERCPU_ENT,
} mt;
struct bpf_mem_alloc *ma;
struct {
struct htab_elem *next; /* 释放链表 */
void *ptr;
} ll;
};
u32 hash; /* 完整哈希值(加速键比较) */
u32 key_size;
/* 紧接 key + value 数据 */
char key_and_value[] __aligned(8); /* 柔性数组成员 */
};哈希映射流程:
- 根据 key 计算 hash:内核使用
jhash()或jhash2()生成 32-bit 哈希值。 hash & (n_buckets - 1)确定 bucket 索引(n_buckets是 2 的幂,位运算替代取模)。- 在
buckets[index]的hlist_nulls_head链表上遍历,比对htab_elem->hash和 key 内容。
下面是 hash bucket 链表的布局示意:
flowchart LR
subgraph buckets["buckets 数组"]
b0["bucket[0]\nhead: * → elem01\nlock"]
b1["bucket[1]\nhead: *\nlock"]
b2["bucket[2]\nhead: * → elem21\nlock"]
end
subgraph elems["htab_elem 链表节点"]
e01["elem01\nhash: 0x...3f\nkey...\nvalue...\nnext → elem02"]
e02["elem02\nhash: 0x...1a\nkey...\nvalue...\nnext → NULL"]
e21["elem21\nhash: 0x...88\nkey...\nvalue...\nnext → NULL"]
end
b0 --> e01
e01 --> e02
b2 --> e21
每个 bucket 有自己独立的
raw_spinlock_t。这意味着不同 bucket
上的操作可以并行——hash map 的并发度等于 bucket 数量。
3.2 预分配(Preallocation)策略
Hash map 的 map_alloc
路径(htab_map_alloc())有一个关键决策:是否预分配元素。
/* kernel/bpf/hashtab.c — htab_map_alloc() 简化逻辑 */
static struct bpf_map *htab_map_alloc(union bpf_attr *attr)
{
struct bpf_htab *htab;
...
/*
* prealloc 判断逻辑:
* - 如果 attr->map_flags 设置了 BPF_F_NO_PREALLOC,则不预分配
* - 如果元素数量 * 元素大小 <= 一页(PAGE_SIZE),则预分配
* - 如果运行在非抢占内核(!CONFIG_PREEMPT),则不预分配
* (因为此时 GFP_ATOMIC 实际上等同于 GFP_KERNEL 所以不会失败)
*/
bool prealloc = !(attr->map_flags & BPF_F_NO_PREALLOC);
if (prealloc) {
/* 在 map 创建时一次性分配所有元素 */
htab->elems = bpf_map_area_alloc(
htab->elem_size * attr->max_entries,
htab->map.numa_node);
/* 初始化每个 elem 并将其加入 freelist */
prealloc_init(htab);
} else {
/* 使用 bpf_mem_alloc 按需分配 */
err = bpf_mem_alloc_init(&htab->ma, htab->elem_size, false);
}
...
}两种模式的核心差异:
| 维度 | prealloc | no-prealloc (BPF_F_NO_PREALLOC) |
|---|---|---|
| 内存时机 | map 创建时一次分配 | update_elem 时按需分配 |
| 分配上下文 | 进程上下文(GFP_KERNEL) |
BPF 运行上下文(GFP_ATOMIC 或
GFP_NOWAIT) |
| 确定性 | 写入延迟完全确定 | 写入可能因内存碎片而失败 |
| 内存占用 | max_entries * elem_size(满额) |
按实际使用量增长 |
| 写入失败 | 只在 freelist 为空时(map 满) | 可能因 GFP_ATOMIC 分配失败(即使 map 未满) |
| 适用场景 | 对延迟敏感的 XDP/TC 路径 | 稀疏使用、大 value、内存紧张的场景 |
关键工程决策:prealloc 模式用更多内存换了确定性延迟。在
XDP 数据面上,GFP_ATOMIC
失败可能意味着丢包,因此生产部署几乎总是使用 prealloc。
Prealloc 模式的 freelist 在 prealloc_init()
中构建:
/* kernel/bpf/hashtab.c — prealloc_init() 简化 */
static void prealloc_init(struct bpf_htab *htab)
{
u32 num_entries = htab->map.max_entries;
int i;
for (i = 0; i < num_entries; i++) {
/* 从连续内存块获取第 i 个元素 */
htab_elem = get_htab_elem(htab, i);
/* 初始化 key_size(后续用于哈希值比对时的字段大小) */
htab_elem->key_size = htab->map.key_size;
}
}注意:prealloc 的 freelist 是隐式的——通过
bpf_mem_cache_alloc() 从
htab->ma
分配,元素不在任何链表中时就是”空闲”状态。实际的空闲管理由
bpf_mem_alloc
的内存池机制负责,不是链表维护的。
3.3 写入路径:htab_map_update_elem
/* kernel/bpf/hashtab.c — htab_map_update_elem() 简化 */
static long htab_map_update_elem(struct bpf_map *map, void *key,
void *value, u64 map_flags)
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
struct htab_elem *l_new, *l_old;
struct bucket *b;
u32 key_size, hash;
int ret;
/* 1. 计算 hash 并定位 bucket */
hash = htab_map_hash(key, key_size, htab->hashrnd);
b = __select_bucket(htab, hash);
/* 2. 获取 bucket 自旋锁 */
raw_spin_lock_irqsave(&b->raw_lock, flags);
/* 3. 查找是否已存在该 key */
l_old = lookup_elem_raw(&b->head, hash, key, key_size);
/* 4. 分配新元素(prealloc 模式从 freelist 取;no-prealloc 调用 kmalloc) */
l_new = alloc_htab_elem(htab, key, value, key_size, false, l_old);
/* 5. 原子替换:旧 → 新 */
if (l_old) {
hlist_nulls_replace_rcu(&l_old->hash_node, &l_new->hash_node);
/* 释放旧元素 */
free_htab_elem(htab, l_old);
} else {
hlist_nulls_add_head_rcu(&l_new->hash_node, &b->head);
}
raw_spin_unlock_irqrestore(&b->raw_lock, flags);
return 0;
}关键细节: - Per-bucket
锁:raw_spin_lock_irqsave(&b->raw_lock, flags)
只锁当前 bucket。不同 key hash 到不同 bucket
时可以完全并行。 - RCU 链表操作:使用
hlist_nulls_replace_rcu /
hlist_nulls_add_head_rcu,保证读侧(lookup)在无锁情况下不会走到野指针。
- 原子替换语义:对于已存在的
key,替换是原子的——新元素入链、旧元素出链,RCU grace period
后释放旧元素。
3.4 无锁读路径:htab_map_lookup_elem
/* kernel/bpf/hashtab.c — htab_map_lookup_elem() 简化 */
static void *htab_map_lookup_elem(struct bpf_map *map, void *key)
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
struct hlist_nulls_head *head;
struct htab_elem *l;
u32 hash, key_size;
/* 注意:BPF 程序执行时 preempt_disable() 已经生效,
* 因此这里不需要显式的 RCU 读锁。
*/
hash = htab_map_hash(key, key_size, htab->hashrnd);
head = &__select_bucket(htab, hash)->head;
/* 遍历链表,比对 hash 和 key */
l = lookup_elem_raw(head, hash, key, key_size);
if (l)
return l->key_and_value + round_up(key_size, 8); /* 返回 value 指针 */
return NULL;
}关键设计:lookup 不加锁。它依赖 RCU
保证在遍历链表时不会遇到已释放的节点——因为写入者在替换元素后通过
call_rcu() 延迟释放旧元素,等待所有 CPU
完成一个 grace period(此时没有 CPU
持有对该元素的引用)后才释放。
在 BPF 程序上下文中,preempt_disable()
已经在 BPF 执行入口生效,这隐含了 RCU 读锁的保护。
对比常规内核哈希表(如 linux/hashtable.h)和
BPF hash map:
| 特性 | 内核 hash_table |
BPF BPF_MAP_TYPE_HASH |
|---|---|---|
| 并发读 | RCU 保护 | RCU 保护(自动) |
| 并发写 | 调用者负责加锁 | per-bucket 自旋锁 |
| 内存分配 | 调用者管理 | prealloc 或 bpf_mem_alloc |
| 遍历 | hash_for_each_possible() |
map_get_next_key() 顺序扫描所有 bucket |
| 预分配 | 无 | 可选的 prealloc |
| 最大元素限制 | 无强制 | max_entries |
3.5 删除操作
/* kernel/bpf/hashtab.c — htab_map_delete_elem() 简化 */
static long htab_map_delete_elem(struct bpf_map *map, void *key)
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
struct bucket *b = __select_bucket(htab, hash);
struct htab_elem *l;
raw_spin_lock_irqsave(&b->raw_lock, flags);
l = lookup_elem_raw(&b->head, hash, key, key_size);
if (l) {
hlist_nulls_del_rcu(&l->hash_node);
free_htab_elem(htab, l);
}
raw_spin_unlock_irqrestore(&b->raw_lock, flags);
return 0;
}删除操作和更新操作共享同一个 bucket 锁,保证对同一个 bucket 的写操作序列化。
3.6 遍历:map_get_next_key
BPF map 的遍历不是通过迭代器,而是通过反复调用
map_get_next_key()。这要求 hash map
记住”当前位置”(通过 key 参数传入的当前
key),然后找到下一个。
/* kernel/bpf/hashtab.c — htab_map_get_next_key() 简化逻辑 */
/*
* 1. 如果 key == NULL,返回第一个非空 bucket 的第一个元素
* 2. 否则:
* a. 从当前 key 所在 bucket 找到当前元素的下一个
* b. 如果当前 bucket 没有更多元素,扫描后续 bucket
* c. 返回第一个找到的元素的 key
*/遍历在无锁(RCU 读)下进行,因此在遍历过程中元素可能被并发删除——这是”弱一致性”遍历,调用者必须接受这个语义。
四、BPF_MAP_TYPE_ARRAY:一维连续内存与零拷贝共享
Array map(BPF_MAP_TYPE_ARRAY)是第二常用的
BPF map 类型。和 hash map 不同,array map 使用 index
而非任意 key 寻址。内核实现位于
kernel/bpf/arraymap.c。
/* kernel/bpf/arraymap.c — Linux 6.8 */
struct bpf_array {
struct bpf_map map;
u32 elem_size; /* value_size */
u32 index_mask; /* max_entries - 1(用于位运算代替取模) */
struct bpf_array_aux *aux;
/* value 数组紧随此结构体 */
char value[] __aligned(8); /* max_entries * elem_size */
};4.1 内存布局:一个扁平的连续数组
Array map 在创建时分配一个连续内存块:
/* kernel/bpf/arraymap.c — array_map_alloc() 简化 */
static struct bpf_map *array_map_alloc(union bpf_attr *attr)
{
u32 elem_size, array_size;
struct bpf_array *array;
elem_size = round_up(attr->value_size, 8);
array_size = sizeof(*array) + attr->max_entries * elem_size;
/* 分配连续内存:结构体 + 所有 value */
array = bpf_map_area_alloc(array_size, numa_node);
...
array->elem_size = elem_size;
array->index_mask = attr->max_entries - 1;
return &array->map;
}sizeof(struct bpf_array) + max_entries * elem_size
——所有 value 紧接在结构体之后。因此:
- 第
i个元素的内存地址是array->value + i * elem_size - 查找是纯粹的内存偏移计算,不涉及任何哈希运算或链表遍历
4.2 零拷贝查找
/* kernel/bpf/arraymap.c — array_map_lookup_elem() */
static void *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 array->value + (u64)index * array->elem_size;
}没有锁、没有 RCU dereference、没有哈希计算——只有一次边界检查和一个指针偏移。在 BPF 程序上下文中,这通常编译为 2-3 条本地指令。
零拷贝的完整含义:BPF 程序得到的指针直接指向内核中 array
map 的内存。用户态通过 BPF_MAP_LOOKUP_ELEM
syscall 读同一块内存时,内核做一次
copy_to_user()。但用户态还可以通过
mmap() 将 array map
映射到用户空间,实现真正的零拷贝读写:
/* kernel/bpf/arraymap.c — array_map_mmap() */
static int array_map_mmap(struct bpf_map *map, struct vm_area_struct *vma)
{
struct bpf_array *array = container_of(map, struct bpf_array, map);
return remap_vmalloc_range(vma, array, vma->vm_pgoff);
}此时用户态进程可以直接读写 array map 的内存,和 BPF 程序看到的是同一块物理内存。这是 array map 独有的能力——hash map 不支持 mmap,因为元素分散在链表中。
4.3 固定大小与无删除的语义
Array map 的关键约束:
- 不能扩展:
max_entries在创建时固定,超出索引的访问返回NULL。 - 不能删除:
map_delete_elem对 array map 返回-EINVAL。要”清空”一个元素,只能update为零值。 - 索引必须从 0 开始:key 是
u32索引,范围[0, max_entries)。
4.4 写入路径
/* kernel/bpf/arraymap.c — array_map_update_elem() 简化 */
static long array_map_update_elem(struct bpf_map *map, void *key,
void *value, u64 map_flags)
{
struct bpf_array *array = container_of(map, struct bpf_array, map);
u32 index = *(u32 *)key;
if (unlikely(index >= array->map.max_entries))
return -E2BIG;
/* 使用 WRITE_ONCE 保证单次写入的原子性(对对齐的 32/64-bit 值) */
if (array->elem_size == 8)
*(u64 *)(array->value + (u64)index * array->elem_size) = *(u64 *)value;
else
memcpy(array->value + (u64)index * array->elem_size, value,
array->elem_size);
return 0;
}注意 array map 的 update 没有加锁。为什么?
- 每个 index 对应一个固定大小的内存槽位。
- 写操作是固定大小的内存复制,不会改变结构(不像 hash map 的链表插入可能改变指针)。
- 对于
elem_size <= 8且对齐的情况,用WRITE_ONCE做单个对齐写,在 x86 上是原子的(自然对齐的 64-bit 写)。 - 对于更大的 value,
memcpy不是原子的——但这和 hash map 不同。array map 的 value 是原地替换,没有指针改写问题。如果并发写入冲突(两个 CPU 同时写 index 3),某一次写入完全覆盖另一次,而不是结构损坏。
这种设计意味着:array map 对读者零开销,对写者极低开销。在 XDP 程序中,array map 通常用作预计算的路由表——启动时写一次,数据面永远只读。
4.5 Array 与 Hash 的选择对比
| 维度 | BPF_MAP_TYPE_ARRAY |
BPF_MAP_TYPE_HASH |
|---|---|---|
| 寻址方式 | u32 索引 |
任意 key(key_size 可配) |
| 查找复杂度 | O(1),无哈希计算 | O(1) 平均,需要哈希 + 链表遍历 |
| 读并发 | 无锁(连续内存) | RCU 保护(间接访问) |
| 写并发 | 无锁(memcpy,非原子) |
per-bucket 自旋锁 |
| mmap | 支持(零拷贝) | 不支持 |
| 删除 | 不支持(只能置零) | 支持 |
| 动态大小 | 固定 | 固定(但元素数 <= max_entries) |
| 内存布局 | 连续内存 | 分散(每个 htab_elem 独立分配) |
| 遍历 | O(n),简单递增索引 | O(n_buckets + n_elems),扫描所有 bucket |
| 最佳场景 | 预计算表、固定索引配置、频繁读 | 连接跟踪、动态 key、频繁增删 |
经验法则:如果 key 是固定范围的整数(如 CPU 编号、端口号、协议类型),用 array。如果 key 是 IP 地址、进程 PID、连接五元组等稀疏空间,用 hash。
五、Per-CPU 变体:this_cpu_ptr() 的无锁语义
BPF_MAP_TYPE_PERCPU_HASH 和
BPF_MAP_TYPE_PERCPU_ARRAY 是 hash / array 的
per-CPU 变体。它们的核心思路是:每个 CPU
有一份独立的 value 拷贝。
5.1 数据布局
Per-CPU hash map 在 struct bpf_htab 中增加了
per-CPU 数据:
/* kernel/bpf/hashtab.c — per-CPU 相关字段 */
struct bpf_htab {
...
struct bucket *buckets;
void *elems; /* prealloc 模式 */
union {
/* per-CPU 变体:每个 htab_elem 的 value 实际分布在所有 CPU 上 */
struct pcpu_htab_value __percpu *pprev;
struct bpf_mem_alloc ma; /* no-prealloc 模式 */
};
...
};
struct pcpu_htab_value {
u64 ____cacheline_aligned_in_smp value[]; /* cacheline 对齐,避免 false sharing */
};对于 per-CPU array
map(BPF_MAP_TYPE_PERCPU_ARRAY),结构体同样嵌入
per-CPU 标记:
/* kernel/bpf/arraymap.c — per-CPU array 的 value 布局 */
/*
* array->value 指向一个连续内存块,但大小是
* max_entries * elem_size * num_possible_cpus()
*
* 第 i 个元素、第 c 个 CPU 的偏移量:
* (i * num_possible_cpus() + c) * elem_size
* 或者在某些实现中:
* i * (num_possible_cpus() * elem_size) + c * elem_size
*/5.2 读路径:this_cpu_ptr()
在 BPF 程序上下文中,对 per-CPU map 的读取不需要任何锁:
/* kernel/bpf/hashtab.c — __htab_map_lookup_percpu_elem() 简化 */
/* BPF helper 在 CPU N 上执行时: */
static void *__htab_map_lookup_percpu_elem(struct bpf_map *map, void *key, u32 cpu)
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
struct htab_elem *l;
/* 1. 找到 key 对应的 htab_elem(和普通 hash lookup 一样) */
l = __htab_map_lookup_elem(map, key); /* RCU 读保护 */
if (!l)
return NULL;
/* 2. 从 per-CPU 数据中返回当前 CPU 的 value */
return this_cpu_ptr(l->pprev);
/* l->pprev 指向该 htab_elem 的 per-CPU value 数组 */
}this_cpu_ptr() 是 per-CPU 变量的核心宏。在
x86-64 上,它编译为对 gs
段寄存器的偏移访问——不涉及任何原子操作、锁或总线锁定:
/* 伪汇编:this_cpu_ptr(ptr) 在 x86-64 上的效果 */
// mov %gs:ptr_offset, %rax ; 从 gs 段读取当前 CPU 的基址
// add $value_offset, %rax ; 加上 value 在结构中的偏移这套机制保证: - CPU 0 上的 BPF 程序读写的是 CPU 0 的 value 拷贝 - CPU 1 上的 BPF 程序读写的是 CPU 1 的 value 拷贝 - 没有 cache line bouncing——每个 CPU 操作自己 cache line 上的数据
5.3 写路径
/* kernel/bpf/hashtab.c — __htab_percpu_map_update_elem() 简化 */
static long __htab_percpu_map_update_elem(struct bpf_map *map, void *key,
void *value, u64 map_flags,
bool onallcpus)
{
struct bpf_htab *htab = container_of(map, struct bpf_htab, map);
struct htab_elem *l_new, *l_old;
struct bucket *b;
u32 key_size, hash;
int ret;
hash = htab_map_hash(key, key_size, htab->hashrnd);
b = __select_bucket(htab, hash);
raw_spin_lock_irqsave(&b->raw_lock, flags);
l_old = lookup_elem_raw(&b->head, hash, key, key_size);
/* 分配新元素(per-CPU value 的大小是 elem_size * num_possible_cpus()) */
l_new = alloc_htab_elem(htab, key, value, key_size, false, l_old);
if (l_old) {
hlist_nulls_replace_rcu(&l_old->hash_node, &l_new->hash_node);
free_htab_elem(htab, l_old);
} else {
hlist_nulls_add_head_rcu(&l_new->hash_node, &b->head);
}
raw_spin_unlock_irqrestore(&b->raw_lock, flags);
return 0;
}注意:per-CPU hash map 的更新仍然需要 bucket
锁——因为需要修改 hash 链表。但是 value
的填写是在分配时完成的,写入的值由调用方预先准备好所有 CPU
的拷贝。BPF helper 侧的
bpf_percpu_hash_update() 只写入当前 CPU 的
value 分片。
5.4 用户态汇总
用户态读取 per-CPU map 时,得到一个
num_possible_cpus() * value_size
字节的数据块。用户态代码负责汇总:
/* 用户态伪代码:汇总 per-CPU counter */
static u64 sum_percpu_counter(int map_fd, u32 key)
{
int nr_cpus = libbpf_num_possible_cpus();
u64 values[nr_cpus];
u64 total = 0;
bpf_map_lookup_elem(map_fd, &key, values);
for (int i = 0; i < nr_cpus; i++)
total += values[i];
return total;
}5.5 使用场景
Per-CPU counters(最经典场景):
/* BPF 程序:per-CPU 计数器 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} pkt_count SEC(".maps");
SEC("xdp")
int count_packets(struct xdp_md *ctx)
{
u32 key = 0;
u64 *counter;
counter = bpf_map_lookup_elem(&pkt_count, &key);
if (counter)
__sync_fetch_and_add(counter, 1); /* 原子加,但只在本地 CPU 竞争 */
return XDP_PASS;
}注意:即使在 per-CPU map 上,使用
__sync_fetch_and_add 仍然是好习惯。同一 CPU
上如果有 NMI 或更高优先级的中断也执行同一个 BPF
程序(虽然概率极低),不加原子操作可能导致丢失计数。实际上,在没有中断嵌套的情况下,仅
(*counter)++ 即可,但
__sync_fetch_and_add 提供了防御性。
Per-CPU 临时缓冲区:BPF
程序的栈大小限制为 512
字节(MAX_BPF_STACK)。当需要更大的临时空间时(如字符串格式化、数据重组),per-CPU
array 的 value 可以用作临时 buffer:
SEC("tracepoint/syscalls/sys_enter_execve")
int trace_exec(struct trace_event_raw_sys_enter *ctx)
{
u32 key = 0;
char *buf;
buf = bpf_map_lookup_elem(&tmp_buf, &key);
if (!buf)
return 0;
/* buf 是一块 per-CPU 的 512 字节缓冲区,无锁安全 */
bpf_probe_read_user_str(buf, 256, ctx->args[0]);
...
return 0;
}5.6 Per-CPU 与普通 Map 的性能差异
Per-CPU 带来的性能提升来自消除 cache line bouncing。普通 map 的 value 在被多个 CPU 并发访问时,包含该 value 的 cache line 在 CPU 之间迁移,每次迁移需要数十纳秒。Per-CPU map 每个 CPU 操作自己 cache line 上的数据,不存在迁移。
| 场景 | 普通 Array | Per-CPU Array | 差异 |
|---|---|---|---|
| 单 CPU 递增计数器 | 低延迟 | 低延迟 | 相近 |
| 多 CPU 递增同一计数器 | 显著变慢(cache line bouncing) | 保持低延迟 | 数量级差距 |
| 多 CPU 读同一值 | 略慢 | 低延迟 | 明显 |
多 CPU 争用同一计数器时,普通 array 因 cache line 在 CPU 间迁移而显著变慢;per-CPU 变体各写各的 cache line,避免 bouncing。具体数值因 CPU 架构与 workload 而异,但数量级差异在所有 x86 平台上一致。
六、并发模型总结
BPF map 的并发模型可以总结为一个表格:
| Map 类型 | 读并发机制 | 写并发机制 | 删除支持 | mmap |
|---|---|---|---|---|
BPF_MAP_TYPE_HASH |
RCU(无锁读) | per-bucket raw_spinlock |
支持 | 不支持 |
BPF_MAP_TYPE_ARRAY |
自然(连续内存) | 无锁 memcpy(非原子) |
不支持 | 支持 |
BPF_MAP_TYPE_PERCPU_HASH |
RCU + this_cpu_ptr() |
per-bucket 锁(结构)+ per-CPU 免锁(数据) | 支持 | 不支持 |
BPF_MAP_TYPE_PERCPU_ARRAY |
this_cpu_ptr()(无锁) |
this_cpu_ptr()(无锁) |
不支持 | 不支持 |
核心设计原则:
- 读路径尽量无锁:RCU(hash)或连续内存(array)保证读不受写干扰。
- 写路径精细化加锁:hash map 用 per-bucket 锁最大化并行度;array map 利用固定大小内存的天然优势避免加锁。
- Per-CPU 消除竞争:把热点数据的竞争从”所有 CPU 争一个缓存行”变成”各写各的”。
理解这些并发模型,是避免生产级 BPF 程序性能问题的前提。下一篇(第 07 篇)将拆解 ring buffer、bloom filter、LPM trie 和队列/栈等更复杂的 map 类型。
参考
内核源码
- Linux 6.8
kernel/bpf/hashtab.c— hash map 实现(htab_map_alloc、htab_map_lookup_elem、htab_map_update_elem、htab_map_delete_elem) - Linux 6.8
kernel/bpf/arraymap.c— array map 实现(array_map_alloc、array_map_lookup_elem、array_map_update_elem、array_map_mmap) - Linux 6.8
include/linux/bpf.h—struct bpf_map、struct bpf_map_ops、struct bpf_array定义 - Linux 6.8
kernel/bpf/syscall.c—map_create()、map_lookup_elem()、map_update_elem()的 syscall 入口
规范与文档
Documentation/bpf/maps.rst— 内核 BPF map 文档include/uapi/linux/bpf.h—enum bpf_map_type枚举
补充资料
上一篇:JIT 编译器后端:x86-64 / ARM64 的 BPF→Native 翻译(第 05 篇)
下一篇:Map 内核实现(下):ringbuf / bloom / queue-stack / LPM(第 07 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】BPF 并发模型:spinlock、RCU 与 per-CPU 模式
BPF 程序在内核上下文中并发执行——同一程序可能在多个 CPU 同时运行。本文讲清 BPF 环境下的内存模型(BPF_ATOMIC 指令的语义)、bpf_spin_lock 的实现限制、RCU 保护的 map 读取、per-CPU map 的免锁读写,以及中断上下文与进程上下文的执行语义差异。
【eBPF 内核实现深度拆解】Map 内核实现(下):ringbuf / perfbuf / bloom / queue-stack / LPM
拆解性能关键的 ring buffer(mmap 双缓冲与 record 提交语义)、perf event array(perf_event_output 路径)、bloom filter(N_HASH 位图)、queue/stack(链式辅助结构)、LPM trie(前缀树),以及 devmap/cpumap 等重定向 map。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。