Switch-Router

Dive into eBPF (4): map-用户空间与内核空间沟通的桥梁

Published at 2019-12-07 | Last Update 2019-12-07

本文是来回答 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));
}