【eBPF-04】进阶:BCC 框架中 BPF 映射的应用 v2.0——尾调用

这两天有空,继续更新一篇有关 eBPF BCC 框架尾调用的内容。

eBPF 技术很新,能够参考的中文资料很少,而对于 BCC 框架而言,优秀的中文介绍和教程更是凤毛麟角。我尝试去网上检索有关尾调用的中文资料,BCC 框架的几乎没有。即使找到了,这些资料也难以给出可供参考和正确运行的例子。

BCC 框架的中文资料也就图一乐,真正有指导意义的,还得去看 Brendan Gregg 大神的博客和 bcc 项目。

既然如此,我来抛砖引玉,就简单介绍一下 eBPF 尾调用在 BCC 框架中是如何应用的吧。

1 何为尾调用?

引用 ebpf.io 网站的一句介绍:"尾调用允许 eBPF 调用和执行另一个 eBPF 并替换执行上下文,类似于一个进程执行 execve() 系统调用的方式。"

也就是说,尾调用之后,函数不会再返回给调用者了。

那么,**eBPF 为什么要使用尾调用呢?**这是因为,eBPF 的运行栈太有限了(仅有 512 字节),在递归调用函数时(实际上是向运行栈中一节一节地添加栈帧),很容易导致栈溢出。而尾调用恰恰允许在不增加堆栈的情况下,调用一系列函数。这是非常有效且实用的。

你可以使用下面的辅助函数来增加一个尾调用:

c 复制代码
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 map,用于记录一组 eBPF 程序的文件描述符。
  • index 为程序数组中需要调用的 eBPF 程序索引。

2 如何使用尾调用?

关于 BCC 框架,reference_guide.md 给出了一个例子。见 27.map.call()

内核态程序:

c 复制代码
// example.c
BPF_PROG_ARRAY(prog_array, 10);			// A)定义程序数组

int tail_call(void *ctx) {
    bpf_trace_printk("Tail-call\n");
    return 0;
}

int do_tail_call(void *ctx) {
    bpf_trace_printk("Original program\n");
    prog_array.call(ctx, 2);			// B)调用 ID 为 2 的函数
    return 0;
}

用户态程序:

python 复制代码
b = BPF(src_file="example.c")
tail_fn = b.load_func("tail_call", BPF.KPROBE)		# C)尾调用函数定义
prog_array = b.get_table("prog_array")
prog_array[c_int(2)] = c_int(tail_fn.fd)		# D)绑定尾调用函数
b.attach_kprobe(event="some_kprobe_event", fn_name="do_tail_call")

代码解释:

A)尾调用的实现,基于 程序数组(BPF_PROG_ARRAY) 这一映射结构。程序数组也是一个键值对结构(废话,它也是 BPF_MAP 类型之一)。其 key 为自定义索引,用于寻找对应的调用程序;其 value 为尾调用函数的文件描述符 fd

B)调用尾调用函数需要执行 call() 方法,传入程序数组(BPF_PROG_ARRAY)中的 key,用来查找对应的函数 fd。

C)尾调用函数的定义在用户态完成。注意这里有一个易错点:尾调用需要和父调用保持相同的程序类型(这里是 BPF.KPROBE)。

D)绑定尾调用函数到程序数组中。不再赘述。

尾调用示意图如下图:

3 实现一个尾调用程序

明白尾调用则怎么玩之后,接下来,我们一起实现一个稍微复杂一点的尾调用,用来监视系统调用。

例子改编自《Learning eBPF》一书。目前该书还没有中文版本。

内核态程序:

c 复制代码
// tail_hello.c
BPF_PROG_ARRAY(syscall, 300);					// A

int hello(struct bpf_raw_tracepoint_args *ctx) {		// B
    int opcode = ctx->args[1];					// C
    syscall.call((void *)ctx, opcode);				// D
    return 0;
}

int hello_exec(void *ctx) {					// E
    bpf_trace_printk("Executing a program\n");
    return 0;
}

int hello_timer(struct bpf_raw_tracepoint_args *ctx) {		// F
    int opcode = ctx->args[1];
    switch (opcode) {
        case 222:
            bpf_trace_printk("Creating a timer\n");
            break;
        case 226:
            bpf_trace_printk("Deleting a timer\n");
            break;
        default:
            bpf_trace_printk("Some other timer operation\n");
            break;
    }
    return 0;
}

代码解释:

【A】BPF_PROG_ARRAY 宏定义,对应映射类型 BPF_MAP_TYPE_PROG_ARRAY。在这里,命名为 syscall,容量为 300。

【B】即将被用户态代码绑定在 sys_enter 类别的 Tracepoint 上,即当有任何系统调用被执行时,都会触发这个函数。bpf_raw_tracepoint_args 类型的结构体 ctx 存放上下文信息。

译者注:sys_enterraw_syscalls 类型的 Tracepoint;同族还有 sys_exit

详细信息可查看文件:/sys/kernel/debug/tracing/events/raw_syscalls/sys_enter/format

【C】对于 sys_enter 类型的追踪点,其参数第 2 项为操作码,即指代即将执行的系统调用号。这里赋值给变量 opcode

【D】这一步,我们把 opcode 作为索引,进行尾调用,执行下一个 eBPF 程序。

再次提醒,这里的写法是 BCC 优化,在真正编译前,BCC 最终会将其重写为 bpf_tail_call 辅助函数。

【E】hello_execve(),程序数组的一项,对应 execve()系统调用。经由尾调用触发。

【F】hello_timer(),程序数组的一项,对应计时器相关的系统调用。经由尾调用触发。

现在,我们来看一下用户态的程序。

python 复制代码
#!/usr/bin/python3
from bcc import BPF
import ctypes as ct

b = BPF(src_file="tail_hello.c")
b.attach_raw_tracepoint(tp="sys_enter", fn_name="hello")		# A

exec_fn = b.load_func("hello_exec", BPF.RAW_TRACEPOINT)			# B
timer_fn = b.load_func("hello_timer", BPF.RAW_TRACEPOINT)

prog_array = b.get_table("syscall")					# C
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)

b.trace_print()								# D

代码解释:

【A】与前文绑定到 kprobe 不同,这次用户态将 hello() 主 eBPF 程序绑定到 sys_enter 追踪点(Tracepoint)上.

【B】这些 load_func() 方法用来将每个尾调用函数载入内核,并返回尾调用函数的文件描述符。尾调用需要和父调用保持相同的程序类型(这里是 BPF.RAW_TRACEPOINT)。

一定不要混淆,每个尾调用程序本身就是一个 eBPF 程序。

【C】接下来,向我们创建好的 syscall 程序数组中添充条目。大可不必全部填满,如果执行时遇到空的,那也没啥影响。同样的,将多个 index 指向同一个尾调用也是可以的(事实上这段程序就是这样做的,将计时器相关的系统调用指向同一个 eBPF 尾调用)。

译者注:这里的 ct.c_int() 来自 Python 的 ctypes 库,用于 Python 到 C 的类型转换。

【D】不断打印输出,直到用户终止程序。

4 运行这个尾调用程序

运行这个 Python 程序,恭喜你,你可能会得到一段报错:

text 复制代码
/virtual/main.c:22:20: error: incompatible pointer to integer conversion passing 'void *' to parameter of type 'u64' (aka 'unsigned long long') [-Wint-conversion]
    bpf_tail_call_((void *)bpf_pseudo_fd(1, -1), ctx, opcode);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/virtual/include/bcc/helpers.h:552:25: note: passing argument to parameter 'map_fd' here
void bpf_tail_call_(u64 map_fd, void *ctx, int index) {

出现这个问题,说明你的系统上 clang 版本为 15。不信你可以看一下:

bash 复制代码
clang -v

我们可以在这个 issue 中找到问题描述(https://github.com/iovisor/bcc/issues/4642 )。

可以在这个 issue 中找到解决方案(https://github.com/iovisor/bcc/issues/4467 )。

大致意思就是,你尝试将一个 u64 类型的值转换成 void *。这在 clang-14 中仅仅是一个 warning,但是在 clang-15 中就会被认定为一个 error

你可以选择降低 clang 版本来解决这个问题。

运行截图如下所示:

5 总结

尾调用的适当应用,能够使 eBPF 如虎添翼。

然而,内核 4.2 版本才开始支持尾调用,在很长的一段时间内,尾调用和 BPF 的编译过程不太兼容(尾调用需要 JIT 编译器的支持)。直到 5.10 版本才解決了这个问题。

你可以最多链接 33 个尾调用(而每个 eBPF 程序的指令复杂度最大支持 100w)。这样一来,eBPF 终于能够真正发挥出巨大潜力来了。

至此,BCC 框架中 BPF 映射就先告一段落了,后面看经历是否在增加其他 BPF 映射结构的应用。

如果您有问题,欢迎留言讨论!如果您觉得这篇文章还不错,请点一个推荐吧~

相关推荐
cominglately2 小时前
centos单机部署seata
linux·运维·centos
魏 无羡2 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse2 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
木子Linux3 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.8243 小时前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维3 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops3 小时前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功4 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
ldinvicible4 小时前
How to run Flutter on an Embedded Device
linux
YRr YRr5 小时前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu