该教程通过12个简单小节帮助你了解bpftrace的使用。每一小节都是一行的命令,你可以立马运行并看到运行效果。该教程系列用来
介绍bpftrace的概念。关于bpftrace的完整参考,见bpftrace完整参考。
该教程贡献者是Brendan Gregg, Netflix (2018), 基于他的DTrace教程系列 [DTrace Tutorial](https://wiki.freebsd.org/DTrac
e/Tutorial)。
bpftrace -l 'tracepoint:syscalls:sys_enter_*'
“bpftrace -l” 列出所有探测点,并且可以添加搜索项。
# bpftrace -e 'BEGIN { printf("hello world\n"); }'
Attaching 1 probe...
hello world
^C
打印欢迎消息。 运行后, 按Ctrl-C结束。
BEGIN
是一个特殊的探针,在程序开始的执行触发探针执行,你可以使用它设置变量和打印消息头。# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
snmp-pass /proc/cpuinfo
snmp-pass /proc/stat
snmpd /proc/net/dev
snmpd /proc/net/if_inet6
^C
这里我们跟踪文件打开的时候打印进程名和文件名。
tracepoint:syscalls:sys_enter_openat
: 这个是tracepoint探针类型(内核静态跟踪),当进入openat()
系统调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(); }'
Attaching 1 probe...
^C
@[bpftrace]: 6
@[systemd]: 24
@[snmp-pass]: 96
@[sshd]: 125
按Ctrl-C后打印进程的系统调用计数。
在bpftrace结束(如按Ctrl-C)时Maps自动打印出来
# bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 18644/ { @bytes = hist(args->ret); }'
Attaching 1 probe...
^C
@bytes:
[0, 1] 12 |@@@@@@@@@@@@@@@@@@@@ |
[2, 4) 18 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 30 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64, 128) 19 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[128, 256) 1 |@
这里跟踪进程号为18644的进程执行内核函数sys_read()的统计,打印出直方图。
[128, 256)
表示值大于等于128且小于256。后面# bpftrace -e 'kretprobe:vfs_read { @bytes = lhist(retval, 0, 2000, 200); }'
Attaching 1 probe...
^C
@bytes:
(...,0] 0 | |
[0, 200) 66 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[200, 400) 2 |@ |
[400, 600) 3 |@@ |
[600, 800) 0 | |
[800, 1000) 5 |@@@ |
[1000, 1200) 0 | |
[1200, 1400) 0 | |
[1400, 1600) 0 | |
[1600, 1800) 0 | |
[1800, 2000) 0 | |
[2000,...) 39 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
使用内核动态跟踪技术显示read()返回字节数的直方图。
kretprobe:vfs_read
: 这里是kretprobe类型(动态跟踪内核函数返回值)的跟踪vfs_read
系统调用探针。此外还有kprobe类型retval
)表示系统调用sys_read()返回值:即成功读取# bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns[comm] = hist(nsecs -
@start[tid]); delete(@start[tid]); }'Attaching 2 probes...
[...]
@ns[snmp-pass]:
[0, 1] 0 | |
[2, 4) 0 | |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 0 | |
[64, 128) 0 | |
[128, 256) 0 | |
[256, 512) 27 |@@@@@@@@@ |
[512, 1k) 125 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[1k, 2k) 22 |@@@@@@@ |
[2k, 4k) 1 | |
[4k, 8k) 10 |@@@ |
[8k, 16k) 1 | |
[16k, 32k) 3 |@ |
[32k, 64k) 144 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64k, 128k) 7 |@@ |
[128k, 256k) 28 |@@@@@@@@@@ |
[256k, 512k) 2 | |
[512k, 1M) 3 |@ |
[1M, 2M) 1 | |
根据进程名,以直方图的形式显示read()调用花费的时间,时间单位为纳秒。
# bpftrace -e 'tracepoint:sched:sched* { @[probe] = count(); } interval:s:5 { exit(); }'
Attaching 25 probes...
@[tracepoint:sched:sched_wakeup_new]: 1
@[tracepoint:sched:sched_process_fork]: 1
@[tracepoint:sched:sched_process_exec]: 1
@[tracepoint:sched:sched_process_exit]: 1
@[tracepoint:sched:sched_process_free]: 2
@[tracepoint:sched:sched_process_wait]: 7
@[tracepoint:sched:sched_wake_idle_without_ipi]: 53
@[tracepoint:sched:sched_stat_runtime]: 212
@[tracepoint:sched:sched_wakeup]: 253
@[tracepoint:sched:sched_waking]: 253
@[tracepoint:sched:sched_switch]: 510
这里统计5秒内进程级的事件并打印。
sched
探针可以探测调度器的高级事件和进程事件如fork, exec和上下文切换# bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
Attaching 1 probe...
^C
[...]
@[
filemap_map_pages+181
__handle_mm_fault+2905
handle_mm_fault+250
__do_page_fault+599
async_page_fault+69
]: 12
[...]
@[
cpuidle_enter_state+164
do_idle+390
cpu_startup_entry+111
start_secondary+423
secondary_startup_64+165
]: 22122
以99赫兹的频率分析内核调用栈并打印次数统计。
ustack
用来分析# bpftrace -e 'tracepoint:sched:sched_switch { @[kstack] = count(); }'
^C
[...]
@[
__schedule+697
__schedule+697
schedule+50
schedule_timeout+365
xfsaild+274
kthread+248
ret_from_fork+53
]: 73
@[
__schedule+697
__schedule+697
schedule_idle+40
do_idle+356
cpu_startup_entry+111
start_secondary+423
secondary_startup_64+165
]: 305
这里统计进程上下文切换次数。以上输出截断了只输出最上面的两个。
# bpftrace -e 'tracepoint:block:block_rq_issue { @ = hist(args->bytes); }'
Attaching 1 probe...
^C
@:
[0, 1] 1 |@@ |
[2, 4) 0 | |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 0 | |
[64, 128) 0 | |
[128, 256) 0 | |
[256, 512) 0 | |
[512, 1K) 0 | |
[1K, 2K) 0 | |
[2K, 4K) 0 | |
[4K, 8K) 24 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8K, 16K) 2 |@@@@ |
[16K, 32K) 6 |@@@@@@@@@@@@@ |
[32K, 64K) 5 |@@@@@@@@@@ |
[64K, 128K) 0 | |
[128K, 256K) 1 |@@ |
以上是块I/O请求字节数的直方图。
探针的上下文是非常重要的: 即当I/O提交给块设备后所处的上下文。通常位于进程上下文,通过内核的comm可以得到进程名,但是
也可能是内核上下文,(如readahead),这时不能显示预期的进程号和进程名信息。
# cat path.bt
#include <linux/path.h>
#include <linux/dcache.h>
kprobe:vfs_open
{
printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}
# bpftrace path.bt
Attaching 1 probe...
open path: dev
open path: if_inet6
open path: retrans_time_ms
[...]
这里使用内核动态跟踪技术跟踪vfs_read()函数,该函数的(struct path *)作为第一个参数。
arg0
是一个内建变量,表示探针的第一个参数,其含义由探针类型决定。对于kprobe
类型探针,它表示函数的第一个参数。((struct path *)arg0)->dentry->d_name.name
: 这里arg0
作为struct path *
并引用dentry。bfptrace对内核结构跟踪的支持和bcc是一样的,允许使用头文件。这意味着大多数结构是可用的,但是并不是所有的,有时需要手
动增加某些结构的声明。例如这个例子,见dcsnoop tool,包含struct nameidate的声明。在将来,bpftrace会使用新的linux BTF支持,到时候所有结构都将可用。到这里你已经理解了bpstrace的大部分功能,你可以开始使用和编写强大的一行命令。查阅参考手册更多的
功能。