姚益祁

eBPF实现某个内核函数调用次数的统计

工具选择

BCC tools

funccount

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_*'

stackcount

传入的eventname的格式与funccount一致,没有额外添加的内容

使用它就能实现发生某事件时调用栈的计数,最后输出时会打印同样的调用栈的函数签名和该调用栈的出现次数

sudo stackcount ktime_get       # 不区分不同进程的相同函数调用栈
sudo stackcount -P ktime_get    # 这样可以区分不同进程的函数调用栈

注:有些函数调用栈长度非常长,很多函数调用栈又是罕见路径,导致整个函数调用栈的输出会异常长,对于这样的输出可以用火焰图来简化分析

trace

十分强大的多用途工具,针对多个数据源的事件进行跟踪,同时支持kprobes、uprobes、USDT、tracepoints

trace的语法如下:

trace [options] probe [probe ...]

bpftrace

使用bpftrace脚本语言上手,它的代码精炼简洁,且完全覆盖了BCC支持的所有事件源

程序结构

probes /filter/ { actions }

具体来说,由探针、过滤器和动作组成

探针(probe)

探针格式如下:

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)

只有filter表达式为真时探针内的actions才会被执行

例如:

/pid == 123/

表示pid为123的进程才会触发该探针

/pid/

表示pid不为0的进程才会触发该探针

动作(actions)

动作可以由单条语句构成,也可由分号分隔多个语句

例如:

{ $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(); }'
映射表函数(map function)
函数 描述
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

bpftrace进行函数执行时长分析实例

#!/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]);
}

bpftrace内核函数调用次数统计

下面的程序以同名进程为粒度输出了内核函数调用次数统计情况:

#!/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

答疑解惑

总结

上述方案是使用BCC tools和bpftrace实现的内核函数调用次数统计方案,两者的使用复杂度都相当简单,且功能丰富强大

其他可能性

实际上还可以使用BCC提供的C语言环境实现,这样实现的好处是可以提供网络接口,在监控程序中直接使用网络,省去了额外的前端

但同时BCC提供的C语言环境编程接口复杂不少,会降低编写监控程序的代码效率