基于ebpf的性能工具-bpftrace脚本语法

bpftrace 通过高度抽象的封装来使用 ebpf,大多数功能只需要寥寥几笔就可以运行起来,可以很快让我们搞清楚 ebpf 是什么样的,而暂时不关心 ebpf 复杂的内部机理。由于 bpftrace 深受 awk 和 c 的影响,bpftrace 使用起来于 awk 非常相似,那些内核 hook 注入点几乎可以按普通字符串匹配来理解,非常容易上手。
前面我们介绍了如何部署bpftrace工具,并且介绍了如何运行bpftrace脚本,这篇文章将介绍bpftrace脚本的语法。
基于ubuntu22.04-深入浅出 ebpf
基于ebpf的性能工具-bpftrace
bpftrace脚本语法 脚本格式 bpftrace脚本基本格式如下: probe {   actions;} bpftrace语法深受awk的影响,{前的部分相当于awk的condition,{}中的部分相当于awk的action。只不过bpftrace执行actions的条件是触发probe名称指定的事件。 probe是探针的名称,我们知道内核中函数非常多,为了方便,内核对probe做了namespace处理,这里的probe通常是以冒号:分割的一组名称,比如: tracepointtick_stopkprobe:do_sys_open 显然,最后一部分表示的是函数名称,其他部分则是namespace,这样做有两点好处:①便于查找函数;②便于定位不同模块中的同名函数。
bpftrace除了可以监听指定的probe事件,还有两个特殊的probe:begin,end。这与awk类似,它们分别在bpftrace程序执行开始、结束时,无条件的执行一些操作,比如完成一些初始化、清理工作等。
begin{    print(hello world.n);}end {    print(bye world.n);} filter是可选的,有时候我们只需要探测特定条件下函数的行为,比如参数为某个值的时候,就可以用到filter,这需要了解bpftrace如何访问probe的变量,我们稍晚再说。 prbbe参数 ebpf支持的probe:hardware,iter,kfunc,kprobe,software,tracepoint,uprobe。
dynamic tracing ebpf提供了内核和应用的动态trace,分别用于探测函数入口处和函数返回(ret)处的信息。
①面向内核的 kprobe/kretprobe,k = kernel ②面向应用的 uprobe/uretprobe,u = user land kprobe/kretprobe 可以探测内核大部分函数,出于安全考虑,有部分内核函数不允许安装探针,另外也可以配合 offset 探测函数中任意位置的信息。
uprobe/uretprobe 则可以为应用的任意函数安装探针。
动态 trace 技术依赖内核和应用的符号表,对于那些 inline 或者 static 函数则无法直接安装探针,需要自行通过 offset 实现。可以借助 nm 或者 strings 指令查看应用的符号表。
这两种动态 trace 技术的原理与 gdb 类似,当对某段代码安装探针,内核会将目标位置指令复制一份,并替换为 int3 中断, 执行流跳转到用户指定的探针 handler,再执行备份的指令,如果此时也指定了 ret 探针,也会被执行,最后再跳转回原来的指令序列。
kprobe 和 uprobe 可以通过 arg0、arg1... ... 访问所有参数;kretprobe 和 uretprobe 通过 retval 访问函数的返回值。除了基本类型:char、int 等,字符串需通过 str() 函数才能访问。
static tracing 静态 trace,所谓 “静态” 是指探针的位置、名称都是在代码中硬编码的,编译时就确定了。静态 trace 的实现原理类似 callback,当被激活时执行,关闭时不执行,性能比动态 trace 高一些。
① 内核中的静态trace:tracepoint ② 应用中的静态trace: usdt = userland statically defined tracing 静态 trace 已经在内核和应用中饱含了探针参数信息,可以直接通过 args->参数名 访问函数参数。tracepoint 的 参数 format 信息可以通过 bpftrace -v probe 查看:
youyeetoo@youyeetoo:~$ bpftrace -lv tracepointsys_exittracepointsys_exit    long id    long retyouyeetoo@youyeetoo:~$  或者访问debugfs: youyeetoo@youyeetoo:~$ cat /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/formatname: sys_exitid: 348format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:long id; offset:8; size:8; signed:1; field:long ret; offset:16; size:8; signed:1;print fmt: nr %ld = %ld, rec->id, rec->retyouyeetoo@youyeetoo:~$   内置变量 无论 dynamic tracing 或者 static tracing,它们的目的都是监听特定函数调用事件,这些函数即可以在内核中,也可以在用户态的应用或者 lib 中。获知这些函数调用时的参数、返回值就已经实现了开发者大半目标。除此之外,bpfstrace 还内置了一些变量,用户访获得探测对象自身信息。这些变量在 bpftrace 中直接访问即可,如下:
pid / tid:bpftrace或者说ebpf工作在内核,因此这些变量都与内核中进程表示有关。先说tid,内核中线程与进程没做作明确区分,它们都是相同的调度对象task_sruct。tid是thread id的缩写,由于历史原因,在task中的成员是task_sruct.pid。所以对于linux内核,线程=轻量级进程。而pid实际上指的是内核中进程组,由task中的task_sruct.tgid成员表示。也就是说,进程=线程组。 uid / gid:执行函数的用户id、组id。 nsecs:时间戳,纳秒。 elapsed:ebpfs 启动后的纳秒数。 numaid:numa = non-uniform memory access,与多核 cpu 的内存访问相关。 cpu:当前 cpu 编号,从 0 开始。 comm:进程名称,通常为进程可执行文件名。 kstack:内核栈。 ustack: 用户栈。 arg0, arg1, ..., argn:函数参数。 sarg0, sarg1, ..., sargn:函数参数(栈中)。 retval:返回值。 func:函数名,可以在可执行文件的符号表中这个函数名。 probe:探针的完整名称,也就是 bpftrace 中 形如 'kprobe:do_nanosleep' curtask:当前 task struct。 rand:一个无符号 32 位随机数。 cgroup:当前进程的 cgroup,内核资源组,类似 namespace,docker 等虚拟化技术即基于内核提供的这一基础设施。 cpid:子进程 pid,bpftrace 允许通过 -c 指定一个 cmd 运行,然后在该进程上安装 probe。 2, ..., #:bpftrace 程序自身的位置参数 全局变量 全局变量@name,所谓的全局变量:①对所有的probe actions可见,②bpftrace生命周期内可见。
bpftrace支持两种变量形式:
① 简单变量,@name = value;简单变量就是单纯的变量名和值,很容易理解,你可以在脚本中创建任意数量的简单变量。 ② map,@name[key] = value;map非常接近python中的dict,或者c中的数组,但数组索引可以是数字、字符串等。例如借助内置变量tid可以为每个线程记录独立的数值。 测试例子:
kprobe:do_nanosleep {    @start[tid] = nsecs;}kretprobe:do_nanosleep /@start[tid] != 0/ {    printf(slept for %d msn, (nsecs - @start[tid]) / 1000000);    delete(@start[tid]);} 运行效果: youyeetoo@youyeetoo:~$ bpftrace bpf_test.bt attaching 2 probes...slept for 0 msslept for 0 msslept for 0 msslept for 0 msslept for 0 msslept for 0 msslept for 0 ms 临时变量 $name, 只在当前action中有效,超出action的{}不具备记忆能力。
内置函数 bpftrace无法自定义函数,但提供了约36个内置函数,可以在bpftrace脚本的任意位置调用它们。完整的列表可以参考官方文档:(https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md)。
bpftrace的函数非常有限,原因是bpftrace脚本会编译为bytecode,交由内核中的ebpf vm执行,出于安全和效率考虑,ebpf vm不能允许用户执行任意函数,仅允许执行限定的函数,或缺有限的数据。
printf -- printf(fmt, ...)bpftrace的printf函数行为与c语言基本一致,区别在于它只支持有限的格式化字符,不如c语言支持的那么多。 begin{    print(hello world.n);}end {    print(bye world.n);} time -- time(fmt)time函数用于打印当前时间,可以通过参数中的格式化字符串指定,如果没有指定格式化字符串,那么默认格式是%h:%m:%sn。time函数完全兼容strftime的格式化字符,下面列出一些常用项: %s 秒,00-60; %m 分钟,00-59; %i 小时,01-12;%h 小时,00-23; %d 每月的第几天,01-31; %w 星期,0-6, 0 指 星期日; %m 月份,01-12; %y 年份,00-99;%y 完整的年份; 「注意:格式化字符结尾不要忘记换行,否则不会自动清空缓冲区到标准输出,就看不到输出了。」
youyeetoo@youyeetoo:~$ bpftrace -e 'interval1 {time(%y %h:%m:%sn);}'attaching 1 probe...2023 16:35:302023 16:35:312023 16:35:32^c system该函数可以调用 shell,用于 probe 触发其他用户态可执行程序非常有用。下面是一个简单的例子,定时调用 `ps. 查看当前进程: youyeetoo@youyeetoo:~$ bpftrace --unsafe -e 'kprobe:do_nanosleep { system(ps -p %dn, pid); }'attaching 1 probe...    pid tty          time cmd    933 ?        00:00:00 cron^c ustack当使用 uprobe 时,很可能需要关注用户进程的 stack 情况,ustack 函数接受 2 个参数,这两个参数可以同时使用,或者只用 1 个。 mode,stack 模式,可选 bpftrace、perf; limit,一个整数,获取 stack 的最大深度; youyeetoo@youyeetoo:~$ bpftrace -e 'uprobereadline { printf(%sn, ustack(perf, 3)); }'stdin:1:1-21: warning: attaching to uprobe target file '/usr/bin/bash' but matched 2 binariesuprobereadline { printf(%sn, ustack(perf, 3)); }~~~~~~~~~~~~~~~~~~~~attaching 1 probe... 56440bb42690 readline+0 (/usr/bin/bash) 56440bb42690 readline+0 (/usr/bin/bash) 56440bb42690 readline+0 (/usr/bin/bash) 控制语句 bpftrace 也提供了常见的流程控制语句:① 条件语句 ② 循环语句
条件语句 bpftrace的条件语句用法与c语言完全一样: if(condition){  statements;   //a} else {  statements;   //b} 当满足条件时执行 a 处语句,否则执行 b 处语句。当然也可能有以下更简单的形式,没有 else 部分,条件满足时执行 a 处语句,然后执行 b 处语句,否则跳过 a 处语句: if(condition){  statements;   //a}statements;   //b 多个 if-else 也可能连接在一起: if(condition){  statements;   //a} else if(condition){  statements;   //b} else if(condition){  statements;   //c} else {  statements;   //d} 测试样例: begin{    $num = $1;    if($num >= 10){        $result = a;    } else if($num >= 5){        $result = b;    } else {        $result = c    }    printf(result: %sn, $result);    exit();} 测试样例结果: youyeetoo@youyeetoo:~$ bpftrace bpf_test.bt 15attaching 1 probe...result: ayouyeetoo@youyeetoo:~$ bpftrace bpf_test.bt 8attaching 1 probe...result: byouyeetoo@youyeetoo:~$ bpftrace bpf_test.bt 3attaching 1 probe...result: cyouyeetoo@youyeetoo:~$  循环语句 bpftrace 支持一种最常见的循环形式: while(condition){  // do something} 测试样例: begin{    $i = 0;    while($i < 10) {        printf(i = %dn, $i);        $i++    }    exit();} 测试样例结果: youyeetoo@youyeetoo:~$ bpftrace bpf_test.btattaching 1 probe...i = 0i = 1i = 2i = 3i = 4i = 5i = 6i = 7i = 8i = 9  


微软正在自研基于Arm的处理器
压敏电阻好坏怎么判断?
信越化学即将研制成功300nm硅片以及实现SOI硅片的产品化
几种常见的晶体管加速电路
美国对晋华发布禁令 存储芯片DRAM将受影响
基于ebpf的性能工具-bpftrace脚本语法
传感器如何解读大数据
汽车同样需要“护眼”:车灯维护注意事项
L-com自锁型USB 3.0线缆组件,受到强烈震动也能确保安全连接
在未来 5G医疗健康是5G技术的一个重要应用领域
2020区块链行业展望是怎样的
IQ信号浅谈
非接触式位移传感器的工作原理是怎样的
新能源汽车阶梯厚铜柔性电路板审评,为我国电子产品做出巨大贡献
两个测量通道信号之间互相干扰,如何解决?
自制印刷线路板的方法
2020年硅基OLED产线投资规模超200亿元
高通骁龙855和华为麒麟980,性能对比说它胜了一筹
AI赋能智慧物流促进快递业飞快发展,抢占“背后的女人”角色
温度变送器是什么_温度变送器工作原理_温度变送器的安装教程及注意事项