Synopsis

bpftrace [OPTIONS] FILENAME
bpftrace [OPTIONS] -e 'program code'

FILENAME 是“-”的时候,bpftrace 会从标准输入读取程序

Description

bpftrace 是一种高级的追踪语言,它给予 eBFP 在 Linux 中运行。支持静态和动态追踪内核和用户空间的进程。

注意:使用bpftrace需要root权限!

Example

列出所有探针

bpftrace -l 'tracepoint:syscalls:sys_enter_*'

“bpftrace -l” 列出所有探针,并且可以添加搜索项。

  • 探针是用于捕获事件数据的检测点。
  • 搜索词支持通配符,如*?
  • “bpftrace -l” 也可以通过管道传递给grep,进行完整的正则表达式搜索

Hello World

bpftrace -e 'BEGIN {printf("Hello world\n");}'

打印欢迎消息。运行后, 按Ctrl-C结束。

  • BEGIN是一个特殊的探针,在程序开始时触发探针执行(类似awk的BEGIN)。你可以使用它设置变量和打印消息头。
  • 探针可以关联动作,把动作放中。这个例子中,探针被触发时会调用printf()。

文件打开

bpftrace -e 'tracepoint:syscalls:sys_enter_openat{printf("%s %s\n", comm, str(args->filename));}'

在文件打开的时候打印文件名和进程名:

  • 该命令以tracepoint:syscalls:sys_enter_openat开始: 这是tracepoint探针类型(内核静态跟踪),当进入openat()系统调用时执行该探针。相比kprobes探针(内核动态跟踪,在第6节介绍),我们更加喜欢用tracepoints探针,因为tracepoints有稳定的应用程序编程接口。注意:现代linux系统(glibc >= 2.26),open总是调用openat系统调用。
  • comm是内建变量,代表当前进程的名字。其它类似的变量还有pid和tid,分别表示进程标识和线程标识。
  • args是一个包含所有tracepoint参数的结构。这个结构是由bpftrace根据tracepoint信息自动生成的。这个结构的成员可以通过命令bpftrace -vl tracepoint:syscalls:sys_enter_openat找到。
  • args.filename用来获取args的成员变量filename的值。
  • str()用来把字符串指针转换成字符串。

进程级系统调用计数

bpftrace -e 'tracepoint:raw_syscalls:sys_enter'{@[comm]=count();}'

按Ctrl-C后打印进程的系统调用计数。

  • @: 表示一种特殊的变量类型,称为map,可以以不同的方式来存储和描述数据。你可以在@后添加可选的变量名(如@num),用来增加可读性或者区分不同的map。
  • []: 可选的中括号允许设置map的关键字,比较像关联数组。
  • count(): 这是一个map函数 - 记录被调用次数。因为调用次数根据comm保存在map里,输出结果是进程执行系统调用的次数统计。

Maps会在bpftrace结束(如按Ctrl-C)时自动打印出来。

read () 返回值统计

bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 57108/ @bytes = hist(args->ret)'

这里统计进程号为18644的进程执行内核函数sys_read()的返回值,并打印出直方图。

  • /…/: 这里设置一个过滤条件(条件判断),满足该过滤条件时才执行{}里面的动作。在这个例子中意思是只追踪进程号为18644的进程。过滤条件表达式也支持布尔运算,如(“&&“, “||“)。
  • ret: 表示函数的返回值。对于sys_read(),它可能是-1(错误)或者成功读取的字节数。
  • @: 类似于上节的map,但是这里没有key,即[]。该map的名称"bytes"会出现在输出中。
  • hist(): 一个map函数,用来描述直方图的参数。输出行以2次方的间隔开始,如[128, 256)表示值大于等于128且小于256。后面跟着位于该区间的参数个数统计,最后是ascii码表示的直方图。该图可以用来研究它的模式分布。
  • 其它的map函数还有lhist(线性直方图),count(),sum(),avg(),min()和max()。

内核动态跟踪 read () 返回的字节数

bpftrace -e 'kretprobe:vfs_read {@bytes = lhist(retval, 0, 2000, 2000);}'

使用内核动态跟踪技术显示read()返回字节数的直方图。

  • kretprobe:vfs_read: 这是kretprobe类型(动态跟踪内核函数返回值)的探针,跟踪vfs_read内核函数。此外还有kprobe类型的探针(在下一节介绍)用于跟踪内核函数的调用。它们是功能强大的探针类型,让我们可以跟踪成千上万的内核函数。然而它们是"不稳定"的探针类型:由于它们可以跟踪任意内核函数,对于不同的内核版本,kprobe和kretprobe不一定能够正常工作。因为内核函数名,参数,返回值和作用等可能会变化。此外,由于它们用来跟踪底层内核的,你需要浏览内核源代码,理解这些探针的参数和返回值的意义。

  • lhist(): 线性直方图函数:参数分别是value,最小值,最大值,步进值。第一个参数(retval)表示系统调用sys_read()返回值:即成功读取的字节数。

read () 调用的时间

bpftrace -e 'kprobe:vfs_read{@start[tid]} = nsecs' kretprobe:vfs_read/@start[tid]/ {@ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]);}

根据进程名,以直方图的形式显示read()调用花费的时间,时间单位为纳秒。

  • @start[tid]: 使用线程ID作为key。某一时刻,可能有许许多多的read调用正在进行,我们希望为每个调用记录一个起始时间戳。这要如何做到呢?我们可以为每个read调用建立一个唯一的标识符,并用它作为key进行统计。由于内核线程一次只能执行一个系统调用,我们可以使用线程ID作为上述标识符。
  • nsecs: 自系统启动到现在的纳秒数。这是一个高精度时间戳,可以用来对事件计时
  • /@start[tid]/: 该过滤条件检查起始时间戳是否被记录。程序可能在某次read调用中途被启动,如果没有这个过滤条件,这个调用的时间会被统计为now-zero,而不是now-start。
  • delete(@start[tid]): 释放变量。

统计进程级别事件

Options

Program Files

我们可以吧程序保存为一个文件,用一个 .bt 文件存储。

  • 通过脚本文件追踪调用 read 系统调用的进程
#!/usr/bin/bpftrace
tracepoint:syscalls:sys_enter_read{
    printf("%s %d is use read.\n", comm, pid);
}