eBPF 程序运行在内核态,但它不是一座孤岛——它需要与用户态交换数据:追踪事件需要传递到用户态,配置策略需要从用户态下发,跨程序共享需要全局状态。这些需求都通过 eBPF Map 实现。
Map 是 eBPF 生态中最重要的基础设施之一。理解 Map 的类型、特性和使用方式,是编写任何非平凡 eBPF 程序的基础。
一、Map 的本质
1.1 什么是 Map
eBPF Map 是内核中的键值存储,可以被 eBPF 程序和用户态程序同时访问:
1.2 Map 的通用属性
所有 Map 类型共享以下属性:
| 属性 | 说明 |
|---|---|
| type | Map 类型(Hash、Array、RingBuf 等) |
| key_size | 键的大小(字节) |
| value_size | 值的大小(字节) |
| max_entries | 最大条目数 |
| map_flags | 标志位(BPF_F_NO_PREALLOC 等) |
1.3 Map 的定义方式
// 方式一:使用 BTF 定义(推荐,CO-RE 兼容)struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 1024); __type(key, u32); __type(value, struct event);} my_map SEC(".maps");
// 方式二:使用 struct bpf_map_def(传统方式)struct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(u32), .value_size = sizeof(struct event), .max_entries = 1024,};方式一(BTF 定义)是当前推荐的方式,它与 CO-RE 兼容,libbpf 可以自动创建和管理 Map。方式二(bpf_map_def)是旧方式,不支持 CO-RE。
二、Hash Map
2.1 特性
BPF_MAP_TYPE_HASH 是最通用的 Map 类型,实现为内核哈希表:
| 特性 | 说明 |
|---|---|
| 查找复杂度 | O(1) 平均 |
| 键类型 | 任意(需指定 key_size) |
| 值类型 | 任意(需指定 value_size) |
| 预分配 | 默认预分配所有条目 |
| 并发 | 内核 RCU + 自旋锁保护 |
2.2 使用示例:连接跟踪表
// 连接跟踪 Mapstruct conn_key { u32 src_ip; u32 dst_ip; u16 src_port; u16 dst_port; u8 protocol;};
struct conn_value { u64 packets; u64 bytes; u64 last_seen;};
struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 65536); __type(key, struct conn_key); __type(value, struct conn_value);} conn_track SEC(".maps");
SEC("xdp")int track_connections(struct xdp_md *ctx){ struct conn_key key = {}; struct conn_value *val;
// 解析数据包填充 key if (parse_packet(ctx, &key) < 0) return XDP_PASS;
// 查找或创建连接记录 val = bpf_map_lookup_elem(&conn_track, &key); if (val) { // 更新已有记录 __sync_fetch_and_add(&val->packets, 1); val->last_seen = bpf_ktime_get_ns(); } else { // 插入新记录 struct conn_value new_val = { .packets = 1, .last_seen = bpf_ktime_get_ns(), }; bpf_map_update_elem(&conn_track, &key, &new_val, BPF_ANY); }
return XDP_PASS;}2.3 Hash Map 的标志
| 标志 | 值 | 说明 |
|---|---|---|
| BPF_ANY | 0 | 存在则更新,不存在则创建 |
| BPF_NOEXIST | 1 | 仅在键不存在时创建 |
| BPF_EXIST | 2 | 仅在键存在时更新 |
| BPF_F_NO_PREALLOC | 1 << 0 | 不预分配内存(节省内存,但查找可能失败) |
三、Array Map
3.1 特性
BPF_MAP_TYPE_ARRAY 是固定大小的数组,键必须是 4 字节无符号整数(索引):
| 特性 | 说明 |
|---|---|
| 查找复杂度 | O(1) |
| 键类型 | u32(数组索引) |
| 预分配 | 始终预分配 |
| 初始值 | 全零 |
| 用途 | 配置项、全局计数器、跳转表 |
3.2 使用示例:配置项
// 配置 Mapstruct config { u32 action; // 0=PASS, 1=DROP, 2=REDIRECT u32 target_if; // 重定向目标接口 u32 rate_limit; // 速率限制(PPS)};
struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, 1); __type(key, u32); __type(value, struct config);} config_map SEC(".maps");
SEC("xdp")int xdp_filter(struct xdp_md *ctx){ u32 key = 0; struct config *cfg = bpf_map_lookup_elem(&config_map, &key); if (!cfg) return XDP_PASS;
switch (cfg->action) { case 0: return XDP_PASS; case 1: return XDP_DROP; case 2: return bpf_redirect(cfg->target_if, 0); default: return XDP_PASS; }}3.3 Prog Array:尾调用跳转表
BPF_MAP_TYPE_PROG_ARRAY 是 Array Map 的特殊变体,值是 eBPF 程序的 fd,用于尾调用:
struct { __uint(type, BPF_MAP_TYPE_PROG_ARRAY); __uint(max_entries, 8); __type(key, u32); __type(value, u32);} prog_array SEC(".maps");
SEC("xdp")int xdp_dispatcher(struct xdp_md *ctx){ // 根据协议分发到不同处理程序 u8 proto = get_protocol(ctx); bpf_tail_call(ctx, &prog_array, proto); return XDP_PASS; // 尾调用失败时的默认行为}四、Per-CPU Map
4.1 为什么需要 Per-CPU
在多核系统中,普通 Hash/Array Map 的更新操作需要获取自旋锁,在高并发场景下成为性能瓶颈。Per-CPU Map 为每个 CPU 维护独立的副本,更新时无需加锁:
4.2 使用示例:每 CPU 计数器
// Per-CPU 计数器struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, u32); __type(value, u64);} pkt_counter SEC(".maps");
SEC("xdp")int count_packets(struct xdp_md *ctx){ u32 key = 0; u64 *count = bpf_map_lookup_elem(&pkt_counter, &key); if (count) *count += 1; // 无锁更新! return XDP_PASS;}// 用户态读取 Per-CPU Mapu32 key = 0;u64 values[num_cpus];bpf_map_lookup_elem(map_fd, &key, values);
u64 total = 0;for (int i = 0; i < num_cpus; i++) total += values[i];printf("Total packets: %lu\n", total);4.3 Per-CPU Map 的限制
| 限制 | 说明 |
|---|---|
| 值大小 | 不能超过内核页面大小(4KB) |
| 用户态读取 | 需要提供 CPU 数量大小的缓冲区 |
| 一致性 | 不同 CPU 的值可能不一致(最终一致) |
五、Ring Buffer
5.1 Ring Buffer vs Perf Buffer
BPF_MAP_TYPE_RINGBUF(5.8+)是替代 BPF_MAP_TYPE_PERF_EVENT_ARRAY 的新一代事件传输机制:
| 特性 | Perf Buffer | Ring Buffer |
|---|---|---|
| 内存使用 | 每 CPU 独立缓冲区 | 全局共享缓冲区 |
| 事件大小 | 固定(value_size) | 可变(每次可不同) |
| 数据拷贝 | 2 次 | 1 次 |
| 内存浪费 | 高(每 CPU 预分配) | 低(全局共享) |
| 事件顺序 | 无保证 | 全局有序 |
| 推荐度 | 旧方案,不推荐 | 推荐 |
5.2 使用示例:事件传输
struct event { u32 pid; u32 tid; char comm[16]; char filename[256];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); // 256 KB} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_openat")int trace_openat(struct trace_event_raw_sys_enter *ctx){ struct event *e;
// 从 Ring Buffer 预留空间 e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
// 填充事件数据 u64 pid_tgid = bpf_get_current_pid_tgid(); e->pid = pid_tgid >> 32; e->tid = (u32)pid_tgid; bpf_get_current_comm(&e->comm, sizeof(e->comm));
const char *filename = (const char *)ctx->args[1]; bpf_probe_read_user_str(&e->filename, sizeof(e->filename), filename);
// 提交事件 bpf_ringbuf_submit(e, 0);
return 0;}5.3 Ring Buffer 的优势
六、其他 Map 类型
6.1 LRU Hash
BPF_MAP_TYPE_LRU_HASH 自动淘汰最近最少使用的条目,适合有容量限制的缓存:
struct { __uint(type, BPF_MAP_TYPE_LRU_HASH); __uint(max_entries, 10000); __type(key, struct conn_key); __type(value, struct conn_value);} conn_cache SEC(".maps");
// 当条目数达到 max_entries 时,自动淘汰最久未访问的条目// 无需手动删除旧条目6.2 Stack Trace
BPF_MAP_TYPE_STACK_TRACE 存储调用栈,用于性能分析:
struct { __uint(type, BPF_MAP_TYPE_STACK_TRACE); __uint(max_entries, 10000); __type(key, u32); __type(value, struct stack_trace_t); // 自动定义} stack_traces SEC(".maps");
SEC("kprobe/do_sys_open")int trace_open(struct pt_regs *ctx){ u32 pid = bpf_get_current_pid_tgid() >> 32; // 捕获当前调用栈 long stack_id = bpf_get_stackid(ctx, &stack_traces, BPF_F_FAST_STACK); if (stack_id >= 0) { // stack_id 可用于从用户态查询完整调用栈 } return 0;}6.3 Socket Map
BPF_MAP_TYPE_SOCKHASH / BPF_MAP_TYPE_SOCKMAP 用于 Socket 重定向,实现本地 Socket 通信绕过内核协议栈:
struct { __uint(type, BPF_MAP_TYPE_SOCKHASH); __uint(max_entries, 65536); __type(key, struct sock_key); __type(value, u32);} sock_map SEC(".maps");
SEC("sk_msg")int bpf_prog(struct sk_msg_md *msg){ // 根据 Socket 信息重定向到目标 Socket struct sock_key key = { .sip = msg->remote_ip4, .dip = msg->local_ip4, .sport = msg->remote_port, .dport = msg->local_port, }; bpf_msg_redirect_hash(msg, &sock_map, &key, BPF_F_INGRESS); return SK_PASS;}6.4 完整 Map 类型参考
| Map 类型 | 内核版本 | 特点 | 典型用途 |
|---|---|---|---|
| HASH | 4.4 | 通用哈希表 | 连接跟踪、进程信息 |
| ARRAY | 4.4 | 固定大小数组 | 配置项、全局计数器 |
| PERCPU_HASH | 4.6 | 每 CPU 哈希表 | 高频更新统计 |
| PERCPU_ARRAY | 4.6 | 每 CPU 数组 | 每 CPU 计数器 |
| PROG_ARRAY | 4.6 | 程序数组 | 尾调用跳转表 |
| PERF_EVENT_ARRAY | 4.4 | 性能事件数组 | 追踪数据传输(旧) |
| RINGBUF | 5.8 | 环形缓冲区 | 事件传输(推荐) |
| STACK_TRACE | 4.6 | 调用栈存储 | 性能分析 |
| LRU_HASH | 4.10 | LRU 哈希 | 有容量限制的缓存 |
| LRU_PERCPU_HASH | 4.10 | LRU 每 CPU 哈希 | 高频缓存 |
| SOCKHASH | 4.18 | Socket 哈希 | Socket 重定向 |
| SOCKMAP | 4.14 | Socket 数组 | Socket 重定向 |
| DEVMAP | 4.14 | 设备数组 | XDP 重定向 |
| CPUMAP | 4.15 | CPU 数组 | XDP CPU 重定向 |
| XSKMAP | 4.18 | XSK 数组 | AF_XDP Socket |
| QUEUE | 4.20 | FIFO 队列 | 消息传递 |
| STACK | 4.20 | LIFO 栈 | 消息传递 |
| SK_STORAGE | 5.2 | Socket 本地存储 | 每 Socket 私有数据 |
| INODE_STORAGE | 5.10 | Inode 本地存储 | 每 Inode 私有数据 |
| TASK_STORAGE | 5.11 | Task 本地存储 | 每 Task 私有数据 |
| STRUCT_OPS | 5.6 | 结构体操作 | TCP 拥塞控制等 |
七、Map 的用户态操作
7.1 通过 bpftool 操作 Map
# 列出所有 Mapsudo bpftool map list
# 查看 Map 详细信息sudo bpftool map show id 123
# 查看 Map 内容sudo bpftool map dump id 123
# 更新 Map 条目sudo bpftool map update id 123 key 0 0 0 0 value 1 0 0 0
# 查找特定条目sudo bpftool map lookup id 123 key 0 0 0 0
# 删除条目sudo bpftool map delete id 123 key 0 0 0 07.2 通过 libbpf 操作 Map
#include <bpf/libbpf.h>#include <bpf/bpf.h>
// 获取 Map fdint map_fd = bpf_map__fd(skel->maps.my_map);
// 查找u32 key = 1;struct event value;int err = bpf_map_lookup_elem(map_fd, &key, &value);if (err == 0) { printf("Found: pid=%u\n", value.pid);}
// 更新value.pid = 1234;bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);
// 遍历u32 next_key;while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) { bpf_map_lookup_elem(map_fd, &next_key, &value); printf("key=%u pid=%u\n", next_key, value.pid); key = next_key;}
// 删除bpf_map_delete_elem(map_fd, &key);7.3 通过 Go 操作 Map
package main
import ( "fmt" "github.com/cilium/ebpf")
func main() { // 打开 Map m, err := ebpf.NewMapFromFile("/sys/fs/bpf/my_map") if err != nil { panic(err) } defer m.Close()
// 查找 var key uint32 = 1 var value uint64 err = m.Lookup(&key, &value) if err == nil { fmt.Printf("Found: key=%d value=%d\n", key, value) }
// 更新 value = 42 err = m.Update(&key, &value, ebpf.UpdateAny)
// 遍历 var iterKey uint32 iter := m.Iterate() for iter.Next(&iterKey, &value) { fmt.Printf("key=%d value=%d\n", iterKey, value) }}八、Map 性能对比
8.1 查找性能
| Map 类型 | 查找延迟 | 更新延迟 | 适用场景 |
|---|---|---|---|
| HASH | ~100ns | ~200ns | 通用 |
| ARRAY | ~20ns | ~30ns | 索引已知 |
| PERCPU_HASH | ~50ns | ~50ns | 高频更新 |
| PERCPU_ARRAY | ~10ns | ~10ns | 计数器 |
| RINGBUF | N/A | ~50ns | 事件传输 |
| LRU_HASH | ~150ns | ~300ns | 缓存 |
8.2 内存开销
| Map 类型 | max_entries=10000, key=4B, value=64B |
|---|---|
| HASH | ~700 KB(预分配) |
| ARRAY | ~640 KB(始终预分配) |
| PERCPU_HASH | ~700 KB × CPU 数 |
| LRU_HASH | ~1.2 MB(含 LRU 元数据) |
| RINGBUF | 由 max_entries 决定 |
Per-CPU Map 的内存开销与 CPU 数成正比。在 128 核的服务器上,一个 Per-CPU Hash Map 的内存开销是普通 Hash Map 的 128 倍!使用前务必评估内存影响。
九、动手实践
9.1 创建和操作 Map
# 创建一个 Hash Mapsudo bpftool map create /sys/fs/bpf/my_hash \ type hash key 4 value 8 entries 1024 name my_hash
# 插入条目sudo bpftool map update pinned /sys/fs/bpf/my_hash \ key 1 0 0 0 value 42 0 0 0 0 0 0 0
# 查找条目sudo bpftool map lookup pinned /sys/fs/bpf/my_hash \ key 1 0 0 0
# 遍历所有条目sudo bpftool map dump pinned /sys/fs/bpf/my_hash9.2 使用 bpftrace 操作 Map
# 统计每个进程的系统调用次数(使用 Map)sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @syscalls[comm] = count();}interval:s:5 { print(@syscalls); clear(@syscalls);}'
# 统计每个进程的读字节数(使用 Map)sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read/args->ret > 0/ { @bytes[comm] = sum(args->ret);}'9.3 监控 Map 使用情况
# 查看 Map 内存使用sudo bpftool map show id 123# 输出包含:bytes_used, max_entries, key_size, value_size
# 查看系统所有 eBPF Map 的内存占用sudo bpftool map listsudo bpftool map show十、本章小结
上一章剖析了eBPF 验证器的安全保证。 本章详细介绍了 eBPF Map 的所有主要类型:
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| Hash Map | 通用键值存储,O(1) 查找,适合连接跟踪、进程信息 | Hash Map |
| Array Map | 固定大小数组,O(1) 索引访问,适合配置项、计数器 | Array Map |
| Per-CPU Map | 每 CPU 独立副本,无锁更新,适合高频统计 | Per-CPU Map |
| Ring Buffer | 高效事件传输,全局有序,替代 Perf Buffer | Ring Buffer |
| LRU Hash | 自动淘汰,适合有容量限制的缓存 | LRU Hash |
| Special Map | Prog Array(尾调用)、Stack Trace(调用栈)、Socket Map(Socket 重定向) | Special Map |
选择 Map 类型的决策树:
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






