【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 字节),在递归调用函数时(实际上是向运行栈中一节一节地添加栈帧),很容易导致栈溢出。而尾调用恰恰允许在不增加堆栈的情况下,调用一系列函数。这是非常有效且实用的。
你可以使用下面的辅助函数来增加一个尾调用:
long bpf_tail_call(void *ctx, struct bpf_map *prog_array_map, u32 index)
其三个参数的含义分别是:
ctx向被调用者传递当前 eBPF 程序的上下文信息。prog_array_map是一个程序数组(BPF_MAP_TYPE_PROG_ARRAY)类型的 eBPFmap,用于记录一组 eBPF 程序的文件描述符。index为程序数组中需要调用的 eBPF 程序索引。
2 如何使用尾调用?
关于 BCC 框架,reference_guide.md 给出了一个例子。见 27.map.call()
内核态程序:
// 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;
}
用户态程序:
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》一书。目前该书还没有中文版本。
内核态程序:
// 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_enter是raw_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(),程序数组的一项,对应计时器相关的系统调用。经由尾调用触发。
现在,我们来看一下用户态的程序。
#!/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 程序,恭喜你,你可能会得到一段报错:
/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。不信你可以看一下:
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 映射结构的应用。
如果您有问题,欢迎留言讨论!如果您觉得这篇文章还不错,请点一个推荐吧~
【eBPF-04】进阶:BCC 框架中 BPF 映射的应用 v2.0——尾调用的更多相关文章
- Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解
最近一直在整合WebAPI.Winform界面.手机短信.微信公众号.企业号等功能,希望把它构建成一个大的应用平台,把我所有的产品线完美连接起来,同时也在探索.攻克更多的技术问题,并抽空写写博客,把相 ...
- 怎样在IDEA中使用JUnit4和JUnitGenerator V2.0自动生成测试模块
因为项目的需要,所以研究了一下自动生成测试代码.将经验记录下来,总会有用的.我个人认为,好记性不如多做笔记多反思总结. 1. 前提条件 开发环境已正确配置 工程已解决JUnit依赖关系(pom ...
- 25.怎样在IDEA中使用JUnit4和JUnitGenerator V2.0自动生成测试模块
转自:https://blog.csdn.net/wangyj1992/article/details/78387728 因为项目的需要,所以研究了一下自动生成测试代码.将经验记录下来,总会有用的.我 ...
- python3开发进阶-Djamgo框架中的JSON和AJAX
阅读目录 什么是JSON 什么是AJAX AJAX常见的应用情景 jQery实现AJAX AJAX请求如何设置csrf_token AJAX上传文件 补充Django内置的serializers 一. ...
- python3开发进阶-Django框架中的ORM的常用操作的补充(F查询和Q查询,事务)
阅读目录 F查询和Q查询 事务 一.F查询和Q查询 1.F查询 查询前的准备 class Product(models.Model): name = models.CharField(max_leng ...
- python3开发进阶-Django框架中的ORM的常用(增,删,改,查)操作
阅读目录 如何在Django终端打印SQL语句 如何在Python脚本中调用Django环境 操作方法 单表查询之神奇的下划线 ForeignKey操作 ManyToManyField 聚合查询和分组 ...
- python3开发进阶-Django框架中form的查看校验方法is_valid()的源码,自定义验证方法
form表单的校验方法is_valid() 点开我们发现这个函数里面只有两个方法方法,最终返回True or False 我们点进.is_bound属性,里面判断传输的数据不是空和上传文件不是空 点进 ...
- 简单理解laravel框架中的服务容器,服务提供者以及怎样调用服务
laravel被称为最优雅的框架,最近正在学习中,对于用惯了thinkphp.ci框架的人来说,服务容器.服务提供者,依赖注入这些概念简直是一脸懵逼.我花了些时间梳理了一下,也不敢确定自己说的是对 ...
- Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式
在我这个系列中,我主要以我正在开发的云会员管理系统为例进行介绍Web API的应用,由于云会员的数据设计是支持多个商家公司,而每个公司又可以包含多个店铺的,因此一些字典型的数据需要考虑这方面的不同.如 ...
- Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用
前面几篇介绍了Web API的基础信息,以及如何基于混合框架的方式在WInform界面里面整合了Web API的接入方式,虽然我们看似调用过程比较复杂,但是基于整个框架的支持和考虑,我们提供了代码生成 ...
随机推荐
- 《流畅的Python》 读书笔记 第7章_函数装饰器和闭包
第7章 函数装饰器和闭包 装饰器这个名称可能更适合在编译器领域使用,因为它会遍历并注解句法树 函数装饰器用于在源码中"标记"函数,以某种方式增强函数的行为.这是一项强大的功能,但是 ...
- 2021北京智源大会SNN部分
神经形态视觉计算 当前问题: spikes vs bits (脉冲 vs 位) meurons vs memory (神经元 vs 计算单元)(真空管vacuum tube,晶体管transistor ...
- 对象转url参数
对象转url function getParams(params) { let paramStr = ''; Object.keys(params) .forEach((item) => { i ...
- JavaSE中的一些面试题
list与Set区别 List 和 Set 是 Java 集合中两个重要的接口,它们在数据存储.数据查找.数据操作等方面有以下几个不同点: 1. 数据存储方式不同:List 是一个有序的 Collec ...
- 用友U8与MES系统API接口对接案例分析
企业数字化转型:轻易云数据集成平台助力 U8 ERP+MES 系统集成 为什么选择数字化转型? 领导层对企业资源规划(ERP)的深刻理解促使了数字化转型的启动. 采用精确的"N+5" ...
- 31. 干货系列从零用Rust编写正反向代理,HTTP限流的实现(limit_req)
wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...
- .NET微信网页开发相关文章教程
前言 今天我们主要总结一下.NET微信网页开发的相关文章教程. 微信网页开发详细文档可以看微信官方文档:https://developers.weixin.qq.com/doc/offiaccount ...
- JavaScript String对象及方法总结
String 对象创建方法: new String() var txt1 = new String("string"); var txt2 = "string" ...
- sudo: unable to execute /bin/rm: Argument list too long
Linux,删除文件夹下所有内容,数据太多时,报错too long sudo rm -r /var/lib/jenkins/workspace/test_1/allure-report/data/at ...
- 如何保障Cookie的信息安全
一.支持策略 保障 Cookie 的安全性可以从以下几个方面进行: 使用 HttpOnly 属性:设置 HttpOnly 属性可以防止JavaScript Document.cookie API 无法 ...