迫于Linux eBPF文档过少,我边学习边把对其的理解记录下来,供后来者参考。
本文是eBPF系列的第三篇:eBPF map。
- 若对Linux tracing技术不清晰,可参考前置篇the Overview of Linux Tracing Tools
- 若对eBPF的工作流程不清晰,可参考eBPF系列一:Hello eBPF
- 篇二提供了一个采集系统调用openat2参数信息的例子,参见eBPF系列二:例子——openat2
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()
cmdBPF_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:
1 | struct msg { |
Note:
- 注意下变量
init_val
在声明的同时也对其进行了初始化操作,若不进行初始化,会报错:”invalid indirect read from stack off -40+4 size 32”,这是在载入eBPF时特意做的检查,目的是阻止因内存未初始化导致的潜在安全风险[3]bpf_map_update_elem()
使用了flagBPF_NOEXIST
,他能确保key对应的value不存在,对于array类型的eBPF map它不可用;这里也可使用BPF_ANY
替代
BPF_MAP_TYPE_HASH
类型的它是同步非阻塞的,也就是说没有办法得知有没有新的数据产生,需要轮询key用以检查是否有新数据的产生,因此用户态程序得这么写用于获取eBPF程序传递的信息:
1 | for (int key = 0; ; key = (key+1)%nr_cpus) { |
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的文件描述符数量。有关源码如下:
1 | struct msg { |
Note:
- 这里的
seq
代表的是消息序列号- 若用户态不向内核态传递消息,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()
即可使用:
1 | static void print_bpf_output(void *ctx, int cpu, void *data, __u32 size) { |
Other eBPF maps
另一类与perf_event_array类似的eBPF map是BPF_MAP_TYPE_RINGBUF
,它可以看作perf_event_array的加强版[8]。此外,还有一类PERCPU
、LRU
前缀的eBPF maps,顾名思义:PERCPU
是per-cpu类型的map,能够减少eBPF程序中的锁竞争,而LRU则是采用了LRU替换算法的map。这些形形色色的map,都可以在linux源码中的samples/bpf[9]目录下找到对应的例子。