funccount
可以实现函数调用次数统计功能,它是一个简洁的命令行工具
它同时支持内核态和用户态函数(它可以使用tracepoints、USDT、kprobes、uprobes)
注:但需要注意,对于高频事件它会引入较高的额外开销
例如malloc和free每秒会调用数百万次,其额外开销会高达30%
默认情况下,它会不断记录某内核函数的调用计数,直到用户按下Ctrl+C
sudo funccount 'vfs_*' # 通配符匹配到的所有内核函数都会被它跟踪
除了Ctrl+C的使用方式,它还可以定时输出该时段内的调用次数
sudo funccount -i 1 c:pthread_mutex_lock # 1秒一次输出,c:表示跟踪的是libc库的函数
可以指定使用哪一种事件源
sudo funccount 't:syscalls:sys_enter_*'
传入的eventname的格式与funccount
一致,没有额外添加的内容
使用它就能实现发生某事件时调用栈的计数,最后输出时会打印同样的调用栈的函数签名和该调用栈的出现次数
sudo stackcount ktime_get # 不区分不同进程的相同函数调用栈
sudo stackcount -P ktime_get # 这样可以区分不同进程的函数调用栈
注:有些函数调用栈长度非常长,很多函数调用栈又是罕见路径,导致整个函数调用栈的输出会异常长,对于这样的输出可以用火焰图来简化分析
十分强大的多用途工具,针对多个数据源的事件进行跟踪,同时支持kprobes、uprobes、USDT、tracepoints
trace的语法如下:
trace [options] probe [probe ...]
使用bpftrace脚本语言上手,它的代码精炼简洁,且完全覆盖了BCC支持的所有事件源
probes /filter/ { actions }
具体来说,由探针、过滤器和动作组成
探针格式如下:
type:identifier1[:identifier2[...]]
type指探针类型,有如下类型:
类型 | 缩写 | 描述 |
---|---|---|
tracepoint | t | |
usdt | U | |
kprobe | k | |
kretprobe | kr | |
uprobe | u | |
uretprobe | ur | |
software | s | 内核软件事件 |
hardware | h | PMC事件插桩 |
profile | p | 对全部CPU进行事件采样 |
interval | i | 从一个CPU上周期性报告 |
BEGIN | ||
END |
探针支持通配符
有两个特殊的探针不需要额外的标识符:BEGIN和END,它们在bpftrace程序启动和退出的时候自动执行
只有filter表达式为真时探针内的actions才会被执行
例如:
/pid == 123/
表示pid为123的进程才会触发该探针
/pid/
表示pid不为0的进程才会触发该探针
动作可以由单条语句构成,也可由分号分隔多个语句
例如:
{ $x = 42; printf("$x is %d", $x); }
注:C语言常见的运算符、控制流在bpftrace中都可以使用,但唯独循环是受限的,因为BPF不能容忍死循环程序的运行
它使用
unroll(count){statements}
来替代循环,其中count是一个整数数字常量,最大值为20
probe1 { @start = nsecs; }
probe2 { $duration = nsecs - @start; } # 用以计算两探针间的时间
内置变量 | 类型 | 描述 |
---|---|---|
pid | integer | |
tid | integer | thread id |
uid | integer | user id |
username | string | |
nsecs | integer | 时间戳,单位纳秒 |
elapsed | integer | 时间戳,单位纳秒,从bpftrace启动开始计时 |
cpu | integer | CPU ID |
comm | string | 进程名 |
kstack | string | 内核调用栈信息 |
ustackk | string | 用户态调用栈信息 |
arg0,…argN | integer | 探针的参数 |
args | struct | 探针的参数 |
retval | integer | 某些探针类型的返回值 |
func | string | 被跟踪函数的名字 |
probe | string | 当前探针全名 |
curtask | integer | 内核task_struct的地址 |
cgroup | integer | cgroup ID |
$1,…$N | int、char * | bpftrace程序的传入参数 |
注:利用kstack可以实现内核调用栈次数统计,达到与stackcount一致的效果
sudo bpftrace -e 't:block:block_rq_insert { @[kstack] = count(); }'
主要有内置函数、映射表函数
函数 | 描述 |
---|---|
printf(char *fmt [, …]) | |
time(char *fmt) | 格式化打印时间 |
join(char *arr[]) | 打印字符串数组,以空格分隔 |
str(char *s[, int len]) | 从指针s返回字符串,长度可选 |
kstack(int limit) | 返回一个深度最大为limit的内核态调用栈 |
ustack(int limit) | 返回一个深度最大为limit的用户态调用栈 |
ksym(void *p) | 分析内核地址,返回字符串形式的符号 |
usym(void *p) | 识别用户空间地址,返回字符串形式的符号 |
kaddr(char *name) | 将内核符号名字翻译为地址 |
uaddr(char *name) | 将用户空间符号名字翻译为地址 |
reg(char *name) | 将寄存器值返回 |
ntop([int af,] int addr) | 返回一个字符串表示的IP地址 |
system(char *fmt [, …]) | 执行shell命令 |
cat(char *filename) | 打印文件内容 |
exit() | 退出bpftrace |
注1:可以看到主要是对字符串的处理函数
注2:kstack内置函数与kstack内置变量的区别主要在于前者可以限制栈的输出长度
用例:
sudo bpftrace -e 't:block:block_rq_insert { @[kstack(3), comm] = count(); }'
函数 | 描述 |
---|---|
count() | 对出现次数进行计数 |
sum(int n) | 求和 |
avg(int n) | 求平均 |
min(int n) | 记录最小值 |
max(int n) | 记录最大值 |
stats(int n) | 返回事件次数、平均值和总和 |
hist(int n) | 打印2的幂次方的直方图 |
lhist(int n, int min, int max, int step) | 打印线性直方图 |
delete(@m[key]) | 删除映射表中的键值对 |
print(@m [, top [, div]]) | 删除映射表,可带参数limit和除数 |
clear(@m) | 删除映射表中全部键 |
zero(@m) | 将映射表中所有的值设置为0 |
#!/usr/bin/bpftrace
kprobe:vfs_read
{
@start[tid] = nsecs;
}
kretprobe:vfs_read
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
@us = hist($duration_us);
delete(@start[tid]);
}
下面的程序以同名进程为粒度输出了内核函数调用次数统计情况:
#!/usr/bin/bpftrace
kprobe:vfs_read
{
@cnts[comm] = count();
}
sudo bpftrace fcount.bt
以下是统计结果示例:
...
@cnts[ls]: 16
@cnts[gmain]: 20
@cnts[xsel]: 20
@cnts[xdg-desktop-por]: 21
@cnts[cat]: 27
@cnts[TTNet-AN-IO]: 28
@cnts[NetworkService]: 29
@cnts[kitty:gl0]: 30
@cnts[gsd-media-keys]: 33
@cnts[kitty:sh0]: 62
@cnts[Xwayland]: 65
@cnts[WebExtensions]: 66
@cnts[systemd-journal]: 79
@cnts[ThreadPoolServi]: 109
@cnts[threaded-ml]: 120
@cnts[DetectThread]: 131
@cnts[ThreadPoolForeg]: 148
@cnts[systemd]: 148
@cnts[feishu]: 170
...
@cnts[nvim]: 628
@cnts[ibus-daemon]: 848
@cnts[KMS thread]: 1120
@cnts[Isolated Web Co]: 1432
@cnts[fish]: 1501
@cnts[gnome-shell]: 1927
@cnts[gdbus]: 3423
@cnts[kitty]: 3597
@us = hist($duration_us)
这样的语句?为什么它没有将@us传入作为hist的第一个参数?
答:bpftrace是一个解释型语言,它对映射表函数有特殊的实现方式。这里的语法可以认为@us实际上就是hist的第一个参数,并且它作为修改后的结果返回。
实际上,所有的映射表函数都是这样,返回的映射表变量同时也是传入这个函数的(隐式)第一个参数。
上述方案是使用BCC tools和bpftrace实现的内核函数调用次数统计方案,两者的使用复杂度都相当简单,且功能丰富强大
实际上还可以使用BCC提供的C语言环境实现,这样实现的好处是可以提供网络接口,在监控程序中直接使用网络,省去了额外的前端
但同时BCC提供的C语言环境编程接口复杂不少,会降低编写监控程序的代码效率