4. eBPF 映射(Map)详解:哈希映射、数组映射、Per-CPU映射、栈映射、队列映射,创建与操作API
eBPF 映射(Map)是 eBPF 程序与用户空间之间、以及不同 eBPF 程序之间进行数据交换的核心数据结构。它本质上是一个内核空间中的键值存储系统,支持多种类型以满足不同的性能与语义需求。本章将深入剖析五种最常用的映射类型及其创建与操作 API。
4.1 映射类型与适用场景
eBPF 映射通过类型区分其底层实现与访问语义。下表对比了五种核心映射类型的关键特性:
| 映射类型 | 数据结构 | 键/值大小 | 访问特性 | 典型场景 |
|---|---|---|---|---|
| BPF_MAP_TYPE_HASH | 哈希表 | 固定大小 | O(1) 平均查找,支持遍历 | 连接跟踪、统计计数、配置存储 |
| BPF_MAP_TYPE_ARRAY | 数组 | 固定大小 | O(1) 索引访问,预分配,无锁 | 全局计数器、固定配置表、perf 事件数组 |
| BPF_MAP_TYPE_PERCPU_HASH | Per-CPU 哈希 | 固定大小 | 每个 CPU 独立副本,无锁更新 | 高并发统计、避免锁竞争 |
| BPF_MAP_TYPE_PERCPU_ARRAY | Per-CPU 数组 | 固定大小 | 每个 CPU 独立副本,索引访问 | CPU 本地计数器、快速聚合 |
| BPF_MAP_TYPE_STACK | 栈(LIFO) | 固定大小 | 后进先出,原子 push/pop | 临时数据暂存、回溯路径记录 |
| BPF_MAP_TYPE_QUEUE | 队列(FIFO) | 固定大小 | 先进先出,原子 enqueue/dequeue | 事件缓冲、消息传递、工作队列 |
选型建议: 需要快速随机访问且键范围已知时优先使用 ARRAY;需要动态键且并发写入高时使用 PERCPU_HASH;需要严格顺序处理时使用 QUEUE 或 STACK。
4.2 映射创建与操作 API
eBPF 映射的生命周期管理通过 bpf() 系统调用完成,用户空间使用 libbpf 库封装后的 API 更为便捷。内核态 eBPF 程序则通过辅助函数直接操作映射。
4.2.1 创建映射:bpf_map_create
用户空间创建映射的标准 API 如下:
#include <bpf/libbpf.h>
int bpf_map_create(enum bpf_map_type map_type,
const char *name,
__u32 key_size,
__u32 value_size,
__u32 max_entries,
const struct bpf_map_create_opts *opts);
参数说明:
- map_type:映射类型,如
BPF_MAP_TYPE_HASH。 - name:映射名称,用于调试与 pinning。
- key_size / value_size:键与值的字节大小。
- max_entries:最大元素数量。
- opts:可选参数,如
map_flags、numa_node。
返回值为文件描述符(fd),失败返回负数。
4.2.2 查找/更新/删除:bpf_map_lookup_elem / bpf_map_update_elem / bpf_map_delete_elem
用户空间与内核空间均使用语义一致的 API 名称:
// 用户空间(libbpf)
int bpf_map_lookup_elem(int fd, const void *key, void *value);
int bpf_map_update_elem(int fd, const void *key, const void *value, __u64 flags);
int bpf_map_delete_elem(int fd, const void *key);
// 内核空间(eBPF 辅助函数)
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key);
long bpf_map_update_elem(struct bpf_map *map, const void *key, const void *value, __u64 flags);
long bpf_map_delete_elem(struct bpf_map *map, const void *key);
关键差异: 内核态
bpf_map_lookup_elem 返回指向值的指针,而非拷贝;用户空间返回拷贝。内核态操作可能因内存压力阻塞,需注意上下文限制。
4.2.3 栈与队列专用 API
栈和队列不支持随机访问,仅提供 push/pop 或 enqueue/dequeue 操作:
// 用户空间
int bpf_map_push_elem(int fd, const void *value, __u64 flags); // 栈/队列通用
int bpf_map_pop_elem(int fd, void *value); // 栈/队列通用
int bpf_map_peek_elem(int fd, void *value); // 查看不移除
// 内核空间
long bpf_map_push_elem(struct bpf_map *map, const void *value, __u64 flags);
long bpf_map_pop_elem(struct bpf_map *map, void *value);
long bpf_map_peek_elem(struct bpf_map *map, void *value);
4.3 映射操作流程与数据流
下图展示了 eBPF 程序与用户空间通过映射进行数据交互的完整流程:
4.4 映射并发访问与 Per-CPU 优化
在高流量场景下,标准哈希映射的全局锁会成为瓶颈。Per-CPU 映射为每个 CPU 核心维护独立副本,消除了锁竞争:
- 写入:每个 CPU 只更新自己的副本,无需加锁。
- 读取:用户空间读取时需遍历所有 CPU 副本并求和,但内核态读取仍可指定 CPU。
- 内存开销:值为
value_size * max_entries * nr_cpus,需合理规划。
注意事项: Per-CPU 映射不保证跨 CPU 的原子性聚合。用户空间读取时可能看到不一致的中间状态,建议在读取前暂停 eBPF 程序或使用 seqcount 保护。
4.5 映射固定(Pinning)与生命周期
映射默认随加载它的 eBPF 程序关闭而销毁。通过 BPF 文件系统(通常挂载于 /sys/fs/bpf)固定映射,可实现跨进程共享与持久化:
// 固定映射到 BPF 文件系统
int bpf_obj_pin(int fd, const char *pathname);
// 从 BPF 文件系统获取已固定的映射
int bpf_obj_get(const char *pathname);
典型路径示例:/sys/fs/bpf/globals/conn_track_map。固定后的映射即使加载程序退出,仍保留数据,直到被显式删除或系统重启。
4.6 实战:使用哈希映射统计网络包
以下代码片段展示如何在 XDP 程序中统计每个源 IP 的包数量:
// 内核态 eBPF 程序 (xdp_stats.c)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32); // 源 IP
__type(value, __u64); // 包计数
} src_ip_map SEC(".maps");
SEC("xdp")
int xdp_count_packets(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
struct iphdr *iph;
if (data + sizeof(*eth) > data_end) return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP)) return XDP_PASS;
iph = data + sizeof(*eth);
if ((void*)iph + sizeof(*iph) > data_end) return XDP_PASS;
__u32 key = iph->saddr;
__u64 *count = bpf_map_lookup_elem(&src_ip_map, &key);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
__u64 init = 1;
bpf_map_update_elem(&src_ip_map, &key, &init, BPF_NOEXIST);
}
return XDP_PASS;
}
// 用户空间读取 (user.c)
int map_fd = bpf_object__find_map_fd_by_name(obj, "src_ip_map");
__u32 key = 0, next_key;
__u64 value;
while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
bpf_map_lookup_elem(map_fd, &next_key, &value);
printf("IP: %u.%u.%u.%u -> %llu packets\n",
(next_key >> 0) & 0xFF,
(next_key >> 8) & 0xFF,
(next_key >> 16) & 0xFF,
(next_key >> 24) & 0xFF,
value);
key = next_key;
}
性能提示: 对于高频率计数场景,建议使用
BPF_MAP_TYPE_PERCPU_HASH 替代普通哈希,避免 __sync_fetch_and_add 的原子操作开销。用户空间聚合时再遍历所有 CPU 副本。
4.7 映射类型选择决策树
以下决策树帮助快速选择最合适的映射类型:
高并发优化: 当选择 HASH 或 ARRAY 且写入频率极高时,自动升级为 PERCPU_HASH 或 PERCPU_ARRAY,可显著提升吞吐量。