Dive into eBPF (4): map-用户空间与内核空间沟通的桥梁
本文是来回答 Dive into eBPF(2)中提到的第3个问题,即:
Q3:用户空间和内核空间的程序是如何通过 map 进行通信的 ?
为什么用户空间要和内核空间程序通信呢?答案是:我们将内核程序灌入内核并运行,很多时候还需要从内核提取信息出来,而 map 则是 eBPF 提供的一套通信机制。如下图所示,map 是一个驻留在内核空间的K-V数据库,内核程序将运行的结果存入数据库,而用户程序则通过系统调用将结果读回。
接下来依然通过 sockex1 这个例程来看看 map 是如何工作的?
map 定义
map数据库是驻留在内核空间的,如下所示,它在内核程序代码中定义
/samples/bpf/sockex1_kern.c
struct bpf_map_def SEC("maps") my_map = {
.type = BPF_MAP_TYPE_ARRAY,
.key_size = sizeof(u32),
.value_size = sizeof(long),
.max_entries = 256,
};
bpf_map_def
的字段几乎都是自解释的,map的类型(type),键的长度(key_size),值的长度(value_size),最大容量(max_entries)。
map 的类型对应着不同的数据库数据结构组织,不同类型的 map 有不同存取方式(operations),BPF_MAP_TYPE_ARRAY 是最常用的 map 类型。
用户空间创建 map
在Dive into eBPF(2)我们知道,内核程序编译生成的 .o 文件要被解析成 ELF 文件 load 到内核。为此,map 是放在独有的 ELF 段中,所以上面 my_map 需要用 attribute 进行修饰。
#define SEC(NAME) __attribute__((section(NAME), used))
用户程序通过BPF_MAP_CREATE系统调用创建 map,输入参数为 map 的各个参数,返回值为 map 对应的 fd。在官方例程中,用户空间程序是这样进行 map 创建的。
load_bpf_file
|
|-- load_maps
|
|-- bpf_create_map
int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,
int max_entries, int map_flags)
{
union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries,
.map_flags = map_flags,
};
return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}
内核空间创建 map
内核空间响应 BPF_MAP_CREATE 系统调用,申请内存作为 map。
/kernel/bpf/syscall.c
static int map_create(union bpf_attr *attr)
{
struct bpf_map *map;
int err;
/* find map type and init map: hashtable vs rbtree vs bloom vs ... */
map = find_and_alloc_map(attr);
// code omitted
err = bpf_map_new_fd(map);
return err;
}
内核程序写 map
内核程序通常做的是,将数据写入 map,早 sockex1 的例子中,内核程序通过 bpf_map_lookup_elem() 找到 index 为 KEY 对应的内存,然后对其进行修改
int bpf_prog1(struct __sk_buff *skb)
{
int index = load_byte(skb, ETH_HLEN + offsetof(struct iphdr, protocol));
long *value;
if (skb->pkt_type != PACKET_OUTGOING)
return 0;
value = bpf_map_lookup_elem(&my_map, &index);
if (value)
__sync_fetch_and_add(value, skb->len);
return 0;
}
用户程序读 map
用户程序可以通过 BPF_MAP_LOOKUP_ELEM 系统调用可以读取 map 中特定 KEY 对应的值, 第一个参数即为创建 map 时返回的 fd.
int bpf_lookup_elem(int fd, void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
};
return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}