eBPF笔记(三)——eBPF Map
eBPF Map
eBPF map是一类数据结构, 通过一个映射表在多个eBPF程序之间共享数据.当然也包括用户空间应用程序与内核中运行的eBPF代码进行通讯。
典型用途
- User space写入要由 eBPF 程序检索的配置信息
- 有一个用于存储state的eBPF程序,其它eBPF可以检索这个状态
- 一个eBPF程序将结果或者指标(metrics)写入 map 中,供显示结果的用户空间程序检索输出。
Type
Linux 的 uapi/linux/bpf.h 文件中定义了各种类型的 BPF map,内核文档中有一些关于它们的信息。通常,它们都是键值存储。比如哈希表、环形缓冲区以及 eBPF 程序数组等等数据结构
某些映射 map 类型被定义为数组,这些数组始终以 4 字节索引作为键类型;其他映射是哈希表,可以使用任意数据类型作为键。
有一些映射类型针对特定类型的操作进行了优化,比如:
- first-in-first-out queues (先进先出队列)
- first-in-last-out stacks (先进后出堆栈)
- least-recently-used data storage (最少最近使用,LRU)
- longest-prefix matching (最长前缀匹配,LPM)
- Bloom filters (布隆过滤器)
某些 eBPF 映射类型包含有关特定类型对象的信息。例如,sockmap 和 devmap 保存有关套接字和网络设备的信息,并由与网络相关的 eBPF 程序用于重定向流量。程序数组映射存储一组索引的 eBPF 程序,并且它用于实现尾部调用 (tail call) 其中一个程序可以调用另一个程序。甚至还有一种 map of map 类型来支持存储有关map的信息
一些map types具有per-CPU variants。也就是说,kernel为该映射的每一个 CPU core’s version 使用不同的内存块。其中多个CPU内核可以同时访问同一映射。5.1版本的内核中添加了对部分映射的旋转锁支持。
示例
Hash Table Map
BPF_HASH(counter_table);
int hello(void *ctx) {
u64 uid;
u64 counter = 0;
u64 *p;
uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;//函数返回值是一个u64类型,low32存储UserID,top32存储GroupID.
p = counter_table.lookup(&uid);//返回key:uid对应value的指针。若无,则为NULL
if (p != 0) {
counter = *p;
}
counter++;
counter_table.update(&uid, &counter); //更新哈希表(此处不是严格的C代码(是C++),但BCC会把它转换成“proper C”)
return 0;
}
"""
完整代码:(《Learning-eBPF》chapter2/hello-map.py)
#!/usr/bin/python3
from bcc import BPF
from time import sleep
program = r"""
BPF_HASH(counter_table);
int hello(void *ctx) {
u64 uid;
u64 counter = 0;
u64 *p;
uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
//
p = counter_table.lookup(&uid);
if (p != 0) {
counter = *p;
}
counter++;
counter_table.update(&uid, &counter);
return 0;
}
"""
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
# Attach to a tracepoint that gets hit for all syscalls
# b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")
while True:
sleep(2)
s = ""
for k,v in b["counter_table"].items():
s += f"ID {k.value}: {v.value}\t"
print(s)
Perf and Ring Buffer Maps
BCC 的BPF_PERF_OUTPUT功能,允许将所选结构中的数据写入 perf 环形缓冲区映射中。
BPF_PERF_OUTPUT(output);
struct data_t {
int pid;
int uid;
char command[16];
char message[12];
};
int hello(void *ctx) {
struct data_t data = {};
char message[12] = "Hello World";
data.pid = bpf_get_current_pid_tgid() >> 32;
data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
bpf_get_current_comm(&data.command, sizeof(data.command));
bpf_probe_read_kernel(&data.message, sizeof(data.message), message);
output.perf_submit(ctx, &data, sizeof(data));
return 0;
}
BCC定义了一个宏BPF_PERF_OUTPUT,用来创建用于kernel和user space之间通讯的映射。
Function Calls
在早期,eBPF 程序不允许调用 helper function 以外的函数。为解决这个问题,可以考虑使用“always inline”
static __always_inline void my_function(void *ctx, int val)
通过内联函数的使用,也算是可以解决“can't jump”这个问题.但同时应当注意,在这其中存在一些限制:
如果从多个位置调用该函数,则会导致该函数指令在已编译的可执行文件中出现多个副本。有时编译器可能会选择内联函数以进行优化,这也是可能无法将 kprobe 附加到某些内核函数的原因之一。
从 Linux 4.16 和 LLVM 6.0 开始,取消了要求函数内联的限制,以便 eBPF 程序员可以更自然地编写函数调用。但是,这个称为“BPF to BPF function calls”或“BPF subprograms”的功能目前不受 BCC 框架的支持(当然,如果函数是内联的,则可以继续使用 BCC 的函数。在 eBPF 中,还有另一种将复杂功能分解为更小部分的机制:Tail call
Tail Calls
“tail calls can call and execute another eBPF program and replace the execution context, similar to how the execve() system call operates for regular processes.”
也就是说,执行完Tail Calls之后,execution并不会返回给调用方。
尾部调用可以避免在递归调用函数时一遍又一遍地向堆栈添加帧,这最终可能导致堆栈溢出错误。如果你可以安排你的代码来调用递归函数作为它做的最后一件事,那么与调用函数关联的堆栈帧实际上并没有做任何有用的事情。尾部调用允许在不增加堆栈的情况下调用一系列函数。这在堆栈限制为 512 字节的 eBPF 中特别有用。
尾部调用是使用 bpf_tail_call() 帮助程序函数进行的,函数原型如下:
long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)
三个参数有如下解释:
- ctx 允许将上下文从调用 eBPF 程序传递给被调用方。
- prog_array_map 是 BPF_MAP_TYPE_PROG_ARRAY 类型的 eBPF 映射,其中包含一组用于标识 eBPF 程序的文件描述符。
- index 指示应调用该group中的哪一个eBPF程序。
要注意的是,正如前面所说:“replace the execution context”。只要这个helper function执行成功,它就不会返回了。当前正在运行的 eBPF 程序在堆栈上将被正在调用的程序替换。但同时,如果这个函数调用失败(比如prog_array_map中没有所指ebpf程序),那么调用方将会继续执行后续代码。
用户空间代码必须像往常一样将所有 eBPF 程序加载到内核中,并且它还设置了prog_array_map。
以下为BCC下的示例代码:
BPF_PROG_ARRAY(syscall, 300);
int hello(struct bpf_raw_tracepoint_args *ctx) {
int opcode = ctx->args[1];
syscall.call(ctx, opcode);
bpf_trace_printk("Another syscall: %d", opcode);
return 0;
}
int hello_exec(void *ctx) {
bpf_trace_printk("Executing a program");
return 0;
}
int hello_timer(struct bpf_raw_tracepoint_args *ctx) {
int opcode = ctx->args[1];
switch (opcode) {
case 222:
bpf_trace_printk("Creating a timer");
break;
case 226:
bpf_trace_printk("Deleting a timer");
break;
default:
bpf_trace_printk("Some other timer operation");
break;
}
return 0;
}
int ignore_opcode(void *ctx) {
return 0;
}
首先使用宏BPF_PROG_ARRAY()创建syscall[300],在后续代码中,index of array为opcode(operation code),the value代表的是函数指针。
然后就是即将要绑定到 sys_enter tracepoint 的函数hello()。应当注意:传递给附加到 raw tracepoint (原始追踪点)的eBPF程序的上下文采用此bpf_raw_tracepoint_args结构的形式。
上下文cxt中的args[1]存储这执行的操作码,然后执行syscall.call(ctx, opcode);(注意:在将源代码传递给编译器之前,BCC 将重写此代码行以调用 bpf_tail_call() 帮助程序函数),如若有与opcode匹配的program则会完成tail call,并且不会执行bpf_trace_printk("Another syscall: %d", opcode);;否则调用失败,hello()会继续执行下去。
其余函数的作用简单明了。
完整python代码:
点击查看代码
#!/usr/bin/python3
from bcc import BPF
import ctypes as ct
program = r"""
BPF_PROG_ARRAY(syscall, 300);
int hello(struct bpf_raw_tracepoint_args *ctx) {
int opcode = ctx->args[1];
syscall.call(ctx, opcode);
bpf_trace_printk("Another syscall: %d", opcode);
return 0;
}
int hello_exec(void *ctx) {
bpf_trace_printk("Executing a program");
return 0;
}
int hello_timer(struct bpf_raw_tracepoint_args *ctx) {
int opcode = ctx->args[1];
switch (opcode) {
case 222:
bpf_trace_printk("Creating a timer");
break;
case 226:
bpf_trace_printk("Deleting a timer");
break;
default:
bpf_trace_printk("Some other timer operation");
break;
}
return 0;
}
int ignore_opcode(void *ctx) {
return 0;
}
"""
b = BPF(text=program)
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")
ignore_fn = b.load_func("ignore_opcode", BPF.RAW_TRACEPOINT)
exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)
prog_array = b.get_table("syscall")
prog_array[ct.c_int(59)] = ct.c_int(exec_fn.fd)
prog_array[ct.c_int(222)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(223)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(224)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(225)] = ct.c_int(timer_fn.fd)
prog_array[ct.c_int(226)] = ct.c_int(timer_fn.fd)
# Ignore some syscalls that come up a lot
prog_array[ct.c_int(21)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(22)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(25)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(29)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(56)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(57)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(63)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(64)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(66)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(72)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(73)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(79)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(98)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(101)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(115)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(131)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(134)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(135)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(139)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(172)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(233)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(280)] = ct.c_int(ignore_fn.fd)
prog_array[ct.c_int(291)] = ct.c_int(ignore_fn.fd)
b.trace_print()
这些对 b.load_func() 的调用返回每个尾部调用程序的文件描述符。请注意,尾部调用需要具有与其父级相同的程序类型------此例为BPF.RAW_TRACEPOINT。此外,值得指出的是,每个尾部调用程序本身就是一个 eBPF 程序。

浙公网安备 33010602011771号