Skip to content

Latest commit

 

History

History

3-map

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
title date tags
eBPF系列三:eBPF map
2021-02-15 07:59:51 -0800
linux
tracing tools
eBPF

Blog Post: eBPF系列三: eBPF map

迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。 本文是eBPF系列的第三篇:eBPF map。

Introduction

先前在eBPF程序中向用户态程序传递信息使用的是bpf_trace_printk(),这种方式有局限性:它只能单向通信、参数最多为三个。另一种通信手段eBPF map,则没有上述限制,它被设计成key/value的形式,能够在用户态程序与内核态eBPF程序之间进行双向通信。官方描述[1]:

Maps are a generic data structure for storage of different types of data. They allow sharing of data between eBPF kernel programs, and also between kernel and user-space applications.

eBPF map在使用时有四个参数需要设置:

  • type: eBPF map的类型,最基础的两类是array与hash,区别在于前者预分配空间,后者用时分配
  • key_size: key的字节大小
  • value_size: value的字节大小
  • max_entries: 元素的最大数量

eBPF map通过bpf()对用户态程序提供了五类cmd[1];对于eBPF程序,bpf-helpers也列出了可用的bpf call[2]:

  • bpf() cmd
    • BPF_MAP_CREATE
    • BPF_MAP_LOOKUP_ELEM
    • BPF_MAP_UPDATE_ELEM
    • BPF_MAP_DELETE_ELEM
    • BPF_MAP_GET_NEXT_KEY
  • bpf call
    • 通用
      • bpf_map_lookup_elem()
      • bpf_map_update_elem()
      • bpf_map_delete_elem()
    • perf event array专用
      • bpf_perf_event_{read, read_value}()
      • bpf_perf_event_output()
    • ring buffer专用
      • bpf_ringbuf_output()
      • bpf_ringbuf_reserve()
      • bpf_ringbuf_submit()
      • bpf_ringbuf_discard()
      • bpf_ringbuf_query()

Instance

下面实现了一个eBPF程序,它能够在每次调用到vfs_read()时,打印出当前OS的启动时间与进程名称。相关源码在这里

Example 1: HASH

这里采用的eBPF map的类型为BPF_MAP_TYPE_HASH,key是cpu id,value是struct msg,它用于记录向用户态抛出的数据信息。除了需要记录OS的启动时间与进程名称之外,还增加了一个变量seq用于体现eBPF map的双向通信特点:eBPF程序作为生产者,每次调用vfs_read()seq增加1,eBPF用户态程序作为消费者,每次取得eBPF程序记录的数据后令seq减一,当seq == 0时清除该map中的key:

struct msg {
	__s32 seq;
	__u64 cts;
	__u8 comm[MAX_LENGTH];
};

struct bpf_map_def SEC("maps") map = {
	.type = BPF_MAP_TYPE_HASH,
	.key_size = sizeof(int),
	.value_size = sizeof(struct msg),
	.max_entries = MAX_ENTRIES,
};

SEC("kprobe/vfs_read")
int hello(struct pt_regs *ctx) {
	int key = bpf_get_smp_processor_id() % MAX_ENTRIES;
	unsigned long cts = bpf_ktime_get_ns();
	struct msg *val, init_val = {0};

	val = bpf_map_lookup_elem(&map, &key);
	if (val) {
		val->seq += 1;
		val->cts = cts;
		bpf_get_current_comm(val->comm, sizeof(val->comm));
	} else {
		init_val.seq = 1;
		init_val.cts = cts;
		bpf_get_current_comm(init_val.comm, sizeof(init_val.comm));
		bpf_map_update_elem(&map, &key, &init_val, BPF_NOEXIST);
	}

	return 0;
}

Note:

  1. 注意下变量init_val在声明的同时也对其进行了初始化操作,若不进行初始化,会报错:"invalid indirect read from stack off -40+4 size 32",这是在载入eBPF时特意做的检查,目的是阻止因内存未初始化导致的潜在安全风险[3]
  2. bpf_map_update_elem()使用了flag BPF_NOEXIST,他能确保key对应的value不存在,对于array类型的eBPF map它不可用;这里也可使用BPF_ANY替代

BPF_MAP_TYPE_HASH类型的它是同步非阻塞的,也就是说没有办法得知有没有新的数据产生,需要轮询key用以检查是否有新数据的产生,因此用户态程序得这么写用于获取eBPF程序传递的信息:

	for (int key = 0; ; key = (key+1)%nr_cpus) {
		if (!bpf_map_lookup_elem(map_fd[0], &key, &msg)) {
			fprintf(stdout, "%.4f: @seq=%d @comm='%s'\n",
				(float)msg.cts/1000000000ul, msg.seq, msg.comm);
			msg.seq -= 1;
			if (msg.seq <= 0)
				bpf_map_delete_elem(map_fd[0], &key);
			else
				bpf_map_update_elem(map_fd[0], &key, &msg, BPF_EXIST);
			memset(&msg, 0, sizeof(msg));
		}
	}

Example 2: PERF_EVENT_ARRAY

有时候我们期望eBPF程序能够通知用户态程序数据准备好了,array、hash类型的eBPF map不满足此类使用场景,这时候就轮到BPF_MAP_TYPE_PERF_EVENT_ARRAY了。与普通hash、array类型有些不同,它没有bpf_map_lookup_elem()方法,使用的是bpf_perf_event_output()向用户态传递数据。它的value_size只能是sizeof(u32),代表的是perf_event的文件描述符;max_entries则是perf_event的文件描述符数量。有关源码如下:

struct msg {
	__s32 seq;
	__u64 cts;
	__u8 comm[MAX_LENGTH];
};

struct bpf_map_def SEC("maps") map = {
	.type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
	.key_size = sizeof(int),
	.value_size = sizeof(__u32),
	.max_entries = 0,
};

SEC("kprobe/vfs_read")
int hello(struct pt_regs *ctx) {
	unsigned long cts = bpf_ktime_get_ns();
	struct msg val = {0};
	static __u32 seq = 0;

	val.seq = seq = (seq + 1) % 4294967295U;
	val.cts = bpf_ktime_get_ns();
	bpf_get_current_comm(val.comm, sizeof(val.comm));

	bpf_perf_event_output(ctx, &map, 0, &val, sizeof(val));

	return 0;
}

Note:

  1. 这里的seq代表的是消息序列号
  2. 若用户态不向内核态传递消息,PERF_EVENT_ARRAY map中的max_entries没有意义。该map向用户态传递的数据暂存在perf ring buffer中,而由max_entries指定的map存储空间存放的是perf_event文件描述符,若用户态程序不向map传递perf_event的文件描述符,其值可以为0。用户态程序使用bpf(BPF_MAP_UPDATE_ELEM)将由sys_perf_event_open()取得的文件描述符传递给eBPF程序,eBPF程序再使用bpf_perf_event_{read, read_value}()得到该文件描述符。于此有关的用法见linux kernel下的sample/bpf/tracex6_{user, kern.c}[4][5])。

libbpf[6]提供了PERF_EVENT_ARRAY map在用户态开箱即用的API,它使用了epoll进行封装,仅需调用perf_buffer__new()perf_buffer__poll()即可使用:

static void print_bpf_output(void *ctx, int cpu, void *data, __u32 size) {
	struct msg *msg = data;

	fprintf(stdout, "%.4f: @seq=%d @comm=%s\n",
		 (float)msg->cts/1000000000ul, msg->seq, msg->comm);
}

int main(int argc, char *argv[]) {
	struct perf_buffer_opts pb_opts = {};
	struct perf_buffer *pb;
	...

	pb_opts.sample_cb = print_bpf_output;
	pb = perf_buffer__new(map_fd, 8, &pb_opts);

	while (true) {
		perf_buffer__poll(pb, 1000);
		if (stop)
			break;
	}
	...
}

Other eBPF maps

另一类与perf_event_array类似的eBPF map是BPF_MAP_TYPE_RINGBUF,它可以看作perf_event_array的加强版[8]。此外,还有一类PERCPULRU前缀的eBPF maps,顾名思义:PERCPU是per-cpu类型的map,能够减少eBPF程序中的锁竞争,而LRU则是采用了LRU替换算法的map。这些形形色色的map,都可以在linux源码中的samples/bpf[9]目录下找到对应的例子。

Reference