本文最后更新于:2026年4月3日 晚上
bpftrace的语法类似awk,熟悉awk的朋友这下有福了,一切看起来都是那么亲切。
语法结构 1 bpftrace -e 'BEGIN {} 探针,探针 /条件/ {执行语句} 探针 /条件/ {执行语句} ... END {} '
和awk语法相似,只是中间的每个执行语句是针对当时的探针触发点。 条件不像awk那样支持正则,只有简单的大小判断,字符串相等判断。
一行式实例学习 hello world 1 bpftrace -e 'BEGIN {print("hello world")}'
输出为:
1 2 bpftrace -e 'BEGIN {print("hello world" )}'Attaching 1 probe... hello world
跟踪打开的文件 1 2 3 4 5 # bpftrace - e 'tracepoint:syscalls:sys_enter_openat {printf("%s open %s\n " , comm, str(args->filename));}'Attaching 1 probe... code open /proc/ 117533 / cmdline code open /proc/ 119202 / cmdlineToDesk_Service open /run/ systemd/seats/
这里有同学可能要问,打开文件系统调用不是open吗,这里怎么使用openat,我怎么知道该用openat? 不清楚用哪个open没有关系,可以用以下办法查看有哪些open:
1 2 3 4 5 6 # bpftrace -l "tracepoint:syscalls:sys_enter_open*" tracepoint: syscalls:sys_enter_opentracepoint: syscalls:sys_enter_open_by_handle_attracepoint: syscalls:sys_enter_open_treetracepoint: syscalls:sys_enter_openattracepoint: syscalls:sys_enter_openat2
然后我们来试一下,有哪些open会被触发:
1 2 3 4 5 6 7 Attaching 5 probes...tracepoint: syscalls:sys_enter_openat bpftrace tracepoint: syscalls:sys_enter_openat bpftrace tracepoint: syscalls:sys_enter_openat systemd-oomd tracepoint: syscalls:sys_enter_openat systemd-oomd tracepoint: syscalls:sys_enter_openat systemd-oomd
然后发现只有sys_enter_openat被触发了。
统计open调用次数 统计各个进程open系统调用的次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # bpftrace -e 'tracepoint:syscalls:sys_enter_openat {@[comm] = count();}' Attaching 1 probe... ^C@[chrome] : 30 @[gnome -terminal-]: 34 @[MemoryInfra] : 60 @[Chrome_IOThread] : 90 @[copyq] : 93 @[systemd -journal]: 103 @[systemd -oomd]: 144 @[docker] : 256 @[ToDesk_Service] : 305 @[code] : 310
知识点:
@表示map类型,后面可以接变量名字,也可以不接,comm表示map的key。
count()是maps函数,每次跟踪点触发计数一次,计数结果保存在map中。
maps会在bpftrace结束后自动打印。
统计read返回值分布 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 1963/ { [0 ] 20 || [1 ] 0 | | [2 , 4 ) 0 | | [4 , 8 ) 0 | | [8 , 16 ) 0 | | [16 , 32 ) 0 | | [32 , 64 ) 0 | | [64 , 128 ) 0 | | [128 , 256 ) 10 | | [256 , 512 ) 10 | |
知识点:
/pid==1963/是过滤器
@bytes也是map,只是没有key
sys_exit_read表示系统调用read退出是的跟踪点,args.ret表示返回值
hist是maps函数,统计直方图,它是以2次方的间隔开始统计。还有类似的函数count(), sum(),avg(),min()和max()。
sum,max,min,avg使用例子:
1 2 3 4 5 6 7 8 9 10 # bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 1963/ {@min = min (args .ret ); @max=max (args .ret ); @sum =sum (args .ret ); @avg=avg(args .ret );}' Attaching 1 probe... ^C @avg: 184 @max: 3205 @min: 0 @sum : 13126
系统调用read的耗时统计 同时跟踪read进入和退出,统计时间消耗
1 2 3 4 5 6 7 8 9 # bpftrace -e 'tracepoint:syscalls:sys_enter_read /pid==1963/{ns=hist(nsecs- [512 , 1 K) 13 || [1 K, 2 K) 6 | | [2 K, 4 K) 6 | | [4 K, 8 K) 3 | |
知识点
定时退出bpftrace 1 2 3 4 5 6 7 8 9 # bpftrace - e 'interval:ms:500 {@t ++ ;printf("hello, %dms\n " , @t * 500 )} interval:s:2 {exit()}'Attaching 2 probes... hello, 500ms hello, 1000ms hello, 1500ms hello, 2000ms@t : 4
知识点
interval是一个周期探针,周期性触发一次,执行后面的动作。interval:ms:500表示周期为500ms的探针,interval:s:2表示周期为2s的探针。
exit() 退出bpftrace程序
采样查看内核栈 以99hz的频率,打断所有cpu,记录调用栈,统计次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # bpftrace -e 'profile:hz:99 {@[kstack] = count()}' Attaching 1 probe... ^C@[ sockfd_lookup_light+98 __sys_recvmsg+103 __x64_sys_recvmsg+29 x64_sys_call+8847 do_syscall_64+127 entry_SYSCALL_64_after_hwframe+120 ]: 1 @[ _raw_spin_unlock_irqrestore+33 logi_dj_recv_forward_input_report+323 logi_dj_raw_event+307 hid_input_report+246 hid_irq_in+552 __usb_hcd_giveback_urb+172 usb_giveback_urb_bh+168 tasklet_action_common.isra.0 +218 tasklet_hi_action+31 handle_softirqs+219 __irq_exit_rcu+217 irq_exit_rcu+14 common_interrupt+164 asm_common_interrupt+39 mwait_idle+75 arch_cpu_idle+9 default_idle_call+44 cpuidle_idle_call+339 do_idle+135 cpu_startup_entry+42 start_secondary+297 secondary_startup_64_no_verify+388 ]: 1
知识点
profile:hz:99 所有cpu都以99hz的频率打断触发采样
kstack 当前的调用栈
查看某个内核函数入参和返回值 打印ip_rcv函数的入参和返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 kretfunc :ip_rcv {printf("ret=%d\n" , retval)}'Attaching 2 probes...ret =0 ret =0 lo len=77 ret =0 lo len=81 ret =0 lo len=52 ret =0 enp6s0 len=370 ret =0 lo len=52 ret =0 lo len=52 ret =0
知识点 kretfunc:函数名 - 表示在函数出口添加探测点 retval - 是函数返回值,不一定是int,也可能是其他类型,也能打印。有的函数没有返回值,就没有这个变量。
语法提要
根据上面的一行式,总结一些用到的语法到下面
内置变量和函数
comm - 事件点所处的进程名
pid - 进程id
tid - 线程id
args - 包含跟踪点所有参数的结构体,有哪些成员可以通过-lv查看
str() - 将指针指向的内容转为字符串,如果类型本身就是数组,那么不用str()函数。
probe - 当前触发的probe完整名字
count() - 是maps函数,每次跟踪点触发计数一次
hist() - 是maps函数,统计直方图,它是以2次方的间隔开始统计。还有类似的函数count(), sum(),avg(),min()和max()。
nsecs - 系统启动到现在的纳秒数
interval:ms:500 - 一个周期间隔探针,用来创建脚本级别的间隔或超时时间
exit() - 退出bpftrace程序
profile:hz:99 所有cpu都以99hz的频率打断触发采样
kstack - 当前的调用栈
变量 @表示map类型,后面可以接变量名字,也可以不接,map可以有key,也可以没有key. maps会在bpftrace结束后自动打印出结果。map的作用域是全局的,生命周期和bpftrace一样长。 $表示临时变量,只在每次probe触发时那段代码有效。用于辅助计算存储。
bpftrace的原理 本质的过程是利用内核的ebpf机制,动态插入代码运行。过程简化如下: bpftrace脚本–>编译为bpf字节码–>内核verifier验证–>JIT编译为机器码–>挂载到probe点执行
几大probe的底层原理 kprobe :在目标函数入口把第一条指令替换成 int3(x86 软件断点),触发时保存寄存器上下文,执行 BPF 程序,再恢复执行。kretprobe 则是在函数入口替换返回地址,函数 ret 时跳到 BPF 程序。kfunc(fentry/fexit) :内核编译时在每个函数头部预留了 nop 指令槽(__fentry__),attach 时把 nop 替换成跳转指令,比 kprobe 少一次中断开销。tracepoint :内核源码里用 TRACE_EVENT 宏静态定义,编译成 __tracepoint_xxx 结构体,触发时遍历回调链表执行 BPF 程序。uprobe :和 kprobe 类似,在用户态目标函数地址处替换成 int3,进程执行到该地址时陷入内核,执行 BPF 程序后返回用户态继续执行。
tracepoint、kprobe、kfunc区别 总结就是:有tracepoint用tracepint,能用kfunc用kfunc,否则用kprobe
tracepoint 是代码中预埋的钩子,名称稳定,有参数类型信息,跨内核版本兼容性好。是静态的,通过写代码埋进去的。 所有的系统调用都有tracepoint,跟踪系统调用肯定用tracepoint 通过-v可以直接查看参数
1 2 3 4 5 6 7 8 9 10 11 12 13 # bpftrace -lv "tracepoint:*:sys_enter_openat*" tracepoint:syscalls:sys_enter_openat int __syscall_nr int dfd const char * filename int flags umode_t mode tracepoint:syscalls:sys_enter_openat2 int __syscall_nr int dfd const char * filename struct open_how * how size_t usize
使用参数也很简单
1 2 3 4 5 # bpftrace - e 'tracepoint:syscalls:sys_enter_openat {printf("%s open %s\n " , comm, str(args->filename));}'Attaching 1 probe... code open /proc/ 117533 / cmdline code open /proc/ 119202 / cmdlineToDesk_Service open /run/ systemd/seats/
kprobe/kretprobe 是动态的,动态在任意内核函数入口/出口插桩。访问参数需要看内核代码参数顺序,通过arg0,arg1来访问参数,而且它们都是uint64的,需要自己转换类型。 举例如下:
1 2 3 4 5 6 7 8 > $skb = (struct sk_buff *)arg0; > printf ("len=%d \n" , $skb ->len); }' Attaching 1 probe... len=372 len=391
arg0需要自己转为sk_buff结构。
kfunc/kretfunc kfunc是目前最新的技术,是kprobe的现代替代。它比kprobe性能更好,而且参数可见,访问更方便
1 2 3 # bpftrace -lv 'kfunc:ip6_route_input' kfunc:vmlinux:ip6_route_input struct sk_buff * skb
依然进行上面的跟踪:
1 2 3 4 5 Attaching 1 probe.. .len =104len =104
更方便,不需要去看内核源代码确认参数位置,也不需要结构体转换,直接可以打印参数
疑问 uprobe打印字符串为空 c代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int test (char *name,int n) { int sum = 0 ; for (int i = 0 ; i < n; i++){ sum += i; } return sum; }int main (int argc, char *argv[]) { printf ("test: %d\n" , test("test-100000" , 100000 )); return 0 ; }
bpftrace脚本如下:
1 2 bpftrace - e 'uprobe:./ uprobe_test:test {@start = nsecs;@name = str(arg0);print (str(arg0))} uretprobe:./ uprobe_test:test {$use = nsecs- @start ;printf("%s: %dus\n " , @name , $use / 1000 )}'
结果输出如下:
1 2 3 4 Attaching 2 probes... : 112 us ^C
uprobe点和uretprobe点都没有输出arg0参数。这可能是字符串的坑点,暂不清楚原因,arg1 int参数可以正常输出。
参考 tutorial_one_liners language
关联 [[bpftrace]]
人生苦短,远离bug
Leon, 2026-04-02