Alastair 与2016.12创建了bpftrace。bpftrace 是一款基于BPF和BCC的开源跟踪器。其自带了许多多性能工具和支持文档,同时提供了一个高级编程语言环境,可以用来创建强大的单行程序和小工具。
在安装bpftrace的时候,其会存在以下的目录树。其中提供了工具的文档、man帮助文档、示例文件等等。
├── docs # 参考手册 单行小程序指引
├── libbpf # 库文件
├── src # 前端
│ └── ast #中间代码生成
├── man
│ ├── adoc
│ └── man8 # 帮助文档
├── README
├── INSTALL
└── tools # 工具和实例
使用-h
参数以获取帮助信息
[root@bogon bpftrace]# bpftrace -h
USAGE:
bpftrace [options] filename
bpftrace [options] - <stdin input>
bpftrace [options] -e 'program'
OPTIONS:
-B MODE output buffering mode ('full', 'none')
-f FORMAT output format ('text', 'json')
-o file redirect bpftrace output to file
-d debug info dry run
-dd verbose debug info dry run
-e 'program' execute this program
-h, --help show this help message
-I DIR add the directory to the include search path
--include FILE add an #include file before preprocessing
-l [search] list probes
-p PID enable USDT probes on PID
-c 'CMD' run CMD and enable USDT probes on resulting process
--usdt-file-activation
activate usdt semaphores based on file path
--unsafe allow unsafe builtin functions
-q keep messages quiet
-v verbose messages
--info Print information about kernel BPF support
-k emit a warning when a bpf helper returns an error (except read functions)
-kk check all bpf helper functions
-V, --version bpftrace version
--no-warnings disable all warning messages
ENVIRONMENT:
BPFTRACE_STRLEN [default: 64] bytes on BPF stack per str()
BPFTRACE_NO_CPP_DEMANGLE [default: 0] disable C++ symbol demangling
BPFTRACE_MAP_KEYS_MAX [default: 4096] max keys in a map
BPFTRACE_CAT_BYTES_MAX [default: 10k] maximum bytes read by cat builtin
BPFTRACE_MAX_PROBES [default: 512] max number of probes
BPFTRACE_LOG_SIZE [default: 1000000] log size in bytes
BPFTRACE_PERF_RB_PAGES [default: 64] pages per CPU to allocate for ring buffer
BPFTRACE_NO_USER_SYMBOLS [default: 0] disable user symbol resolution
BPFTRACE_CACHE_USER_SYMBOLS [default: auto] enable user symbol cache
BPFTRACE_VMLINUX [default: none] vmlinux path used for kernel symbol resolution
BPFTRACE_BTF [default: none] BTF file
EXAMPLES:
bpftrace -l '*sleep*'
list probes containing "sleep"
bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'
trace processes calling sleep
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
count syscalls by process name
应用场景 | 工具名称 | 使用场景及用法 |
---|---|---|
CPU | execsnoop.bt |
类型 | 缩写 | 描述 |
---|---|---|
tracepoint | t | 内核静态插桩点 |
usdt | U | 用户静态插桩点 |
kprobe | k | 内核动态函数插桩点 |
kretprobe | kr | 内核动态函数返回值插桩点 |
uprobe | u | 用户动态函数插桩点 |
uretprobe | ur | 用户动态函数返回值插桩点 |
software | s | 内核软件事件 |
hardware | h | 硬件基于计数器的插桩 |
profile | p | 对全部cpu进行采样 |
interval | i | 一个cpu上周期性报告 |
BEGIN | 启动 | |
END | 退出 |
tracepoint 探针类型会对内核跟踪点进行插桩,格式如下:
# tracepoint_name 是跟踪点全名,包括用来将跟踪点所在列和事件名字分割开的冒号
tracepoint:tracepoint_name
跟踪点通常是带有参数的:bpftrace 可以通过内置变量args来访问这些参数信息。假如说我们访问的跟踪点有一个代表数据包长度的参数,名称为len,我们可以用agrs->len
来访问这个变量。而一个跟踪点具体具有哪些参数则是可以bpftrace的-lv
参数来进行查看:
[root@bogon ik]# bpftrace -lv tracepoint:syscalls:sys_enter_read
tracepoint:syscalls:sys_enter_read
int __syscall_nr
unsigned int fd
char * buf
size_t count
我们可以看到这里跟man参数文档描述的其实是不一致的,这里会多一个系统调用号__syscall_nr
。
ssize_t read(int fd,void *buf,size_t count)
udst探针对用户态静态探针点进行插桩。格式如下:
usdt:binary_path:probe_name
usdt:libary_path:probe_name
usdt:binary_path:namespace:probe_name
usdt:libary_path:namespace:probe_name
比如说我们定义了如下的函数:
// 会编译成可执行文件test
namespace bpf_method{
void bpf_test();
}
那么对这个函数来说,其探针可以表述usdt:./test:bpf_method:bpf_test
。另外,如果是在不确定的话,我们可以使用-l
参数列出当前二进制文件之中所有可用的usdt探针。
# 这里存疑,笔者并没有实验成功
bpftrace -l 'usdt:/usr/local/cpython/python'
除此之外,我们还可以使用 -p pid
的方式列出正在运行的程序其支持usdt类型探针:
bpftrace -lp 2266 | grep usdt
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_sbrk_more
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tcache_double_free
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_count
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_max_bytes
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_unsorted_limit
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:setjmp
[...]
这种探针类型用于内核的动态插桩,格式如下:
kprobe:function_name
kretprobe:function_name
kprobe对函数的开始进行插桩,其参数arg0,arg1,arg2…是进入函数时的参数,类型均为64位无符号整数。如果其是指向C结构体的指针,可以进行强制转换; kretprobe对函数的结束进行插桩,其内置参数retval是函数的返回值,类型永远是64位无符号整型,如果和上述整型不一致,需要通过类型强制转换为对应的类型。
这种探针类型用于用户态的动态插桩,格式如下:
uprobe:binary_path:function_name
uprobe:libary_path:function_name
uretprobe:binary_path:function_name
uretprobe:libary_path:function_name
uprobe对函数的开始进行插桩,其参数arg0,arg1,arg2…是进入函数时的参数,类型均为64位无符号整数。如果其是指向C结构体的指针,可以进行强制转换; uretprobe对函数的结束进行插桩,其内置参数retval是函数的返回值,类型永远是64位无符号整型,如果和上述整型不一致,需要通过类型强制转换为对应的类型。
这些探针的类型是预先定义好的软件事件和硬件事件,类型如下:
software:event_name
software:event_name:count
hardware:event_name
hardware:event_name:count
这些事件跟跟踪点是类似的,不过更适合于基于计数器的指标和基于采样的探测。由于事件的频闭可能很高,所以这里存在一个count
字段,当每发生count
此事件之后,才会触发一次探针。如果没有指定count,则会采用默认的频率。
这些是基于定时器的事件,格式如下:
profile:hz:rate
profile:s:rate
profile:ms:rate
profile:us:rate
interval:s:rate
interval:ms:rate
profile类型会在全部cpu之上进行激活,可以用作对CPU的使用进行采样;interval类型只在单个CPU上激活,可以用于周期性的打印输出。比如说profile:hz:100
就表示100hz周期会激活一次对CPU使用的采样。
curl https://repos.baslab.org/bpftools.repo --output /etc/yum.repos.d/bpftools.repo
sudo yum install bpftrace bpftrace-tools
sudo apt-get update
sudo apt-get install bpftrace
下面是个bpftrace编程的例子,其展示了bpftrace 编程的基本结构:
# 指定解释器
#!/usr/local/bin/bpftrace
# 定义的探针1
kprobe:vfs_read
{
# 将当前进程调用vfs_read 的时间放入一个名为start的map之中
@start[tid] = nsecs;
}
# 定义的探针2
kretprobe:vfs_read
# 过滤start 这个map,获取tid这一项
/@start[tid]/
{
# 获取执行时间
$duration_us = (nsecs - @start[tid]) / 1000;
# 执行时间放入直方图的bucket之中
@us = hist($duration_us);
# 删除map中这个元素
delete(@start[tid]);
}
下面是上面这个文件的输出展示,vfs_exec_time.bt
是存放上面代码的文件,bt后缀只是为了辨别:
[root@bogon bpftrace]# bpftrace vfs_exec_time.bt
Attaching 2 probes...
^C
@start[6165]: 6928640480723
@us:
[0] 185 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1] 56 |@@@@@@@@@@@@@@@ |
[2, 4) 91 |@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4, 8) 44 |@@@@@@@@@@@@ |
[8, 16) 1 | |
[16, 32) 3 | |
[32, 64) 2 | |
[64, 128) 0 | |
[128, 256) 0 | |
[256, 512) 0 | |
[512, 1K) 0 | |
[1K, 2K) 1 | |
bpftrace主要有单行程序和批处理两种用法:
# bpftrace 单行程序
bpftrace -e 'program'
-e
参数表示bpftrace会执行program,并开始跟踪其中定义的所有时间。程序会持续运行,直达Ctrl+C
组合键被按下或者程序显示的调用exit()
为止。
bpftrace file.bt
程序还可被保存到一个文件之中,然后使用bpftrace来执行。或者我们也可以使用chmod +x file.bt
来赋予文件可执行权限,这样就可以像其他任何程序一样运行。
bpftrace程序的结构是一系列探针+对应的动作,当探针被激活后,相应的动作就会被执行。
probe { actions }
注: 动作
动作既可以是单条语句,也可以是使用分号分割的多条语句{ action1,action2、action3... }
probe /filter_pattern/ { actions }
注: 过滤器
filter_pattern
是个布尔表达式,其会决定一个动作是否被执行,比如说pid==123
表示只有进程123触碰探针才会执行动作。同时,在布尔表达式中也可以运用&&
、||
等布尔运算符
如果我们希望对多个探针实行同一动作处理,可以在探针之间加上,
来分割,如下:
probe1,probe2,... { actions }
同时,探针也支持通配符,如下:
# 匹配probe1、probeabc等等...
probe* { actions }
探针以类型名字开始,然后是一系列以:
为分隔的标识符:
# ...表示可选
type:identifier1[:identifier2[...]]
一般来说,内核探针kprobe探针类型对内核态函数进行插桩,只需要一个标识符:内核函数名。uprobe探针类型对用户态函数进行插桩,需要两个标识符: 二进制文件的路径和函数名
在bpftrace之中有三种类型的变量:
变量 | 作用 |
---|---|
pid | 进程id |
comm | 进程名称 |
nsecs | 时间戳(纳秒) |
curtask | 当前线程的 task_struct 结构体 |
$
作为前缀# 定义变量x并赋值1
$x = 1
@
前缀,可以用于全局存储,在不同动作之间传递数据。# 定义全局变量a 并赋值1
probe1 { @a = 1;}
# 定义array这个映射表,其为数组,给数组中对应的元素赋值
probe2 { @array[tid] = nsecs}
# 复合数组映射表
probe2 { @muti_array[pid,tid] = nsecs}
bpftrace支持三种控制流:filter、ternarg和if语句,这些都依靠布尔表达式来有条件的改变对程序执行的流向。
probe /filter_pattern/ { actions }
注: 过滤器
filter_pattern
是个布尔表达式,其会决定一个动作是否被执行,比如说pid==123
表示只有进程123触碰探针才会执行动作。同时,在布尔表达式中也可以运用&&
、||
等布尔运算符
bpftrace的 if 语法如下:
if (test) {
actions
}
if (test) {
actions
} else {
other_actions
}
由于bpftrace运行在受限的环境中,所以这里使用了unroll
语句来保证循环时有限的。
bpftrace 的内置函数主要如下:
函数 | 类型 | 作用 |
---|---|---|
exit() | 普通函数 | 退出bpftrace |
str(char*) | 普通函数 | 输出一个指针,返回字符串 |
system(format[,arguments …]) | 普通函数 | 在shell之中运行命令 |
count() | 映射表函数 | 累计统计 |
sum($var) | 映射表函数 | 求和 |
hist($var) | 映射表函数 | 直方图展示 |
delete($var) | 映射表函数 | 删除映射表元素 |
bpftrace -e 'BEGIN { printf("hello world!\n"); }'
[root@bogon bpftrace]# bpftrace -e 'BEGIN { printf("hello world!\n"); }'
Attaching 1 probe...
hello world!
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
# 在这里,笔者打开了另外一个中断,执行了ls 命令,也就是这里最后一行。
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
ksmtuned -> /usr/bin/awk
ksmtuned -> /usr/bin/pgrep
ksmtuned -> /usr/bin/awk
ksmtuned -> /usr/bin/sleep
bash -> /usr/bin/ls
注: 什么是ksmtuned?
KSM 是一种节省内存的重复数据删除功能,由 CONFIG_KSM=y 启用,在 2.6.32 中添加到 Linux 内核中。
KSM 最初是为与 KVM(在那里称为内核共享内存)一起使用而开发的,通过共享它们之间的公共数据,将更多虚拟机放入物理内存中。但它对于生成相同数据的许多实例的任何应用程序都非常有用。
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'
# 下面代码中的第一行展示了笔者在另外的终端运行ls的过程,其余代码则是KSM机制所带来的
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'
Attaching 1 probe...
ls --color=auto
awk /^(MemFree|Buffers|Cached):/ {free += $2}; END {print free} /proc/meminfo
pgrep -d -- ^qemu(-(kvm|system-.+)|:.{1,11})$
awk { sum += $1 }; END { print 0+sum }
sleep 60
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
in:imjournal /run/log/journal/29545612d2ce4e319e6a9edaa0cff1d3/system.journa
systemd-journal /
systemd-journal sys
systemd-journal bus
注: 什么是systemd-journal?
systemd-journald 是一个收集并存储各类日志数据的系统服务。 它创建并维护一个带有索引的、结构化的日志数据库, 并可以收集来自各种不同渠道的日志:
- 通过 kmsg 收集内核日志
- 通过 libc 的 syslog(3) 接口收集系统日志
- 通过 本地日志接口 sd_journal_print(3) 收集结构化的系统日志
- 捕获服务单元的标准输出(STDOUT)与标准错误(STDERR)
- 通过内核审计子系统收集审计记录
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
^C
@[NetworkManager]: 19
@[tuned]: 24
@[sshd]: 24
@[irqbalance]: 40
@[bpftrace]: 67
@[gdbus]: 93
@[in:imjournal]: 673
@[systemd-journal]: 7752
...
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
# 下面的代码会统计运行期间符合‘tracepoint:syscalls:sys_enter_*’ 正则匹配的所有探针的调用次数
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
Attaching 341 probes...
^C
@[tracepoint:syscalls:sys_enter_waitid]: 1
@[tracepoint:syscalls:sys_enter_getpid]: 1
@[tracepoint:syscalls:sys_enter_timerfd_create]: 1
@[tracepoint:syscalls:sys_enter_socket]: 1
@[tracepoint:syscalls:sys_enter_rt_sigreturn]: 1
@[tracepoint:syscalls:sys_enter_munmap]: 2
@[tracepoint:syscalls:sys_enter_mmap]: 2
@[tracepoint:syscalls:sys_enter_sendmsg]: 5
@[tracepoint:syscalls:sys_enter_writev]: 5
@[tracepoint:syscalls:sys_enter_ftruncate]: 5
@[tracepoint:syscalls:sys_enter_inotify_add_watch]: 5
...
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'
Attaching 1 probe...
^C
@[3126, gpg-agent]: 1
@[3171, gpg-agent]: 1
@[3139, gpg-agent]: 1
@[985, sssd]: 1
@[1084, tuned]: 2
@[2848, gmain]: 3
@[2751, gmain]: 4
@[698, systemd-journal]: 2178
...
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'
Attaching 1 probe...
^C
@[sshd]: 54
@[in:imjournal]: 63
@[gmain]: 188
@[sedispatch]: 756
@[systemd-journal]: 18056
bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'
[root@bogon ik]# bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'
Attaching 1 probe...
^C
@[sshd]:
[2, 4) 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[32, 64) 2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
...
bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'
[root@bogon ik]# bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'
Attaching 1 probe...
# pid name bytes
251 kworker/2:1H 4096
284 kworker/4:1H 0
251 kworker/2:1H 0
288 kworker/7:1H 14848
288 kworker/7:1H 0
3684 kworker/2:0 8
3684 kworker/2:0 8
bpftrace -e 'software:major-faults:1 { @[comm] = count(); }'
[root@bogon ik]# bpftrace -e 'software:major-faults:1 { @[comm] = count(); }'
Attaching 1 probe...
^C
@[sssd_nss]: 2
bpftrace -e 'software:faults:1 { @[comm] = count(); }'
[root@bogon ik]# bpftrace -e 'software:faults:1 { @[comm] = count(); }'
Attaching 1 probe...
^C
@[systemd-journal]: 5
@[in:imjournal]: 5
bpftrace -e 'profile:hz:yy /pid == xxx/ { @[ustack] = count(); }'