tracepoint是Linux内核静态定义的一些调试点,它分布于内核的各个子系统中,然而在实际工作中可能很多人并没有用过这个功能,或者对它没有太多了解,那么就通过本文一起来了解下tracepoint吧。tracepoint是内核预先定义的静态探测点,可以用于挂载钩子函数来做trace。当没有钩子函数时,它几乎没有损耗,只有挂载了钩子函数才会真正启用trace功能。这个钩子函数可以由开发者编写内核module来实现,并且需要在钩子函数中获取我们调试所需要的信息并导出到用户态,这样我们就可以获取内核运行时的信息了。
直接使用tracepoint并不是那么的容易,因此内核提供了event trace功能。event trace的实现依赖于tracepoint机制,内核提前帮我们实现了钩子函数并挂到tracepoint上,当使能一个event trace时,它会输出内容到ftrace ringbuffer中,这样就可以获取到内核运行信息了。当然有时候event trace并不符合我们的需要,那么此时就只得自己编写module来实现需求了。
如何查看tracepoint
查看tracepoint有多种方式,比如简单的通过debugfs查看:
/sys/kernel/debug/tracing/events/
系统中定义的tracepoint都在该events目录中,比如查看block子系统中的tracepoint:
ls /sys/kernel/debug/tracing/events/block/
block_bio_backmerge block_bio_complete block_bio_queue block_dirty_buffer block_plug block_rq_complete block_rq_issue block_rq_requeue block_split block_unplug filter
block_bio_bounce block_bio_frontmerge block_bio_remap block_getrq block_rq_abort block_rq_insert block_rq_remap block_sleeprq block_touch_buffer enable
除了这种基础方法,还可以使用perf来查看:
perf list tracepoint
这个命令会打印出来所有的tracepoint。
对于支持bpf的内核版本,还可以bpftrace来查看:
bpftrace -l tracepoint:*
tracepoint数据格式
每个tracepoint都会按照自己定义的格式来输出信息,可以在用户态来查看tracepoint记录的内容格式,比如:
cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_open/format
name: sys_enter_open
ID: 475
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int nr; offset:8; size:4; signed:1;
field:const char * filename; offset:16; size:8; signed:0;
field:int flags; offset:24; size:8; signed:0;
field:umode_t mode; offset:32; size:8; signed:0;
print fmt: "filename: 0x%08lx, flags: 0x%08lx, mode: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->flags)), ((unsigned long)(REC->mode))
格式信息可以用来解析二进制的trace流数据,也可以用这个格式中的内容来做trace filter,利用filter功能指定过滤条件后,将只会看到过滤后的事件数据。格式信息中包括两部分内容,第一部分是通用的格式,这类通用字段都带有common前缀,这是所有的tracepoint event都具备的字段。第二部分就是各个tracepoint所自定义的格式字段了,比如上面的nr,filename等等。格式信息的最后一列是tracepoint的打印内容格式,通过这个可以看到打印数据的字段来源。
如何使用tracepoint
tracepoint复用了ftrace的ringbuffer,当使能了一个tracepoint之后,可以通过cat /sys/kernel/debug/tracing/trace
来查看它的输出。另外上文也有提到,还可以通过设置filter过滤事件。
举例说明,比如想要过滤所有的openat事件,指定过滤所有打开flags为0的事件:
echo flags==0 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/filter
echo 1 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/enable
cat /sys/kernel/debug/tracing/trace_pipe
如果想要把过滤条件清除,只需要写入0到指定的filter即可:
echo 0 > /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/filter
到这里我们就掌握了tracepoint的基本使用方法。
再举一个例子,想要跟踪sched_switch这个tracepoint点,设置过滤条件prev_pid和next_pid都为1的进程。
echo "(prev_pid == 1 || next_pid == 1)" > /sys/kernel/debug/tracing/events/sched/sched_switch/filter
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
cat /sys/kernel/debug/tracing/trace_pipe
输出的示例如下:
<idle>-0 [003] d... 1412218.564007: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=S ==> next_comm=systemd next_pid=1 next_prio=120
systemd-1 [003] d... 1412218.564109: sched_switch: prev_comm=systemd prev_pid=1 prev_prio=120 prev_state=D ==> next_comm=YDEdr next_pid=2184088 next_prio=120
<idle>-0 [003] d... 1412222.984930: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=S ==> next_comm=systemd next_pid=1 next_prio=120
systemd-1 [003] d... 1412222.985006: sched_switch: prev_comm=systemd prev_pid=1 prev_prio=120 prev_state=D ==> next_comm=swapper/3 next_pid=0 next_prio=120
<idle>-0 [002] d... 1412222.987441: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=S ==> next_comm=systemd next_pid=1 next_prio=120
systemd-1 [002] d... 1412222.987513: sched_switch: prev_comm=systemd prev_pid=1 prev_prio=120 prev_state=D ==> next_comm=YDService next_pid=2184066 next_prio=120
可以看到对应的prev_id或者next_pid为1时才会输出
perf中使用tracepoint
借助perf工具,可以跟踪tracepoint事件,前文已经介绍了如何用perf查看tracepoint,假如想要跟踪网络驱动收发包的情况:
perf record -e 'net:netif_rx','net:net_dev_queue' -a -- sleep 10
记录10秒后,使用:
perf script
来查看记录到的信息,截取部分示例如下:
swapper 0 [000] 18846825.547490: net:net_dev_queue: dev=eth0 skbaddr=0xffff881fe98e0800 len=58
fping 13009 [000] 18846825.547921: net:net_dev_queue: dev=eth0 skbaddr=0xffff881fe98e0e00 len=98
fping 13009 [000] 18846825.557986: net:net_dev_queue: dev=eth1 skbaddr=0xffff881fe98e0e00 len=98
bpftrace中使用tracepoint
bpftrace工具也可以跟踪tracepoint事件,前面已经提到,查看bpftrace可跟踪的事件,可以通过:
bpftrace -l tracepoint:*
这里需要掌握的更关键的一点,是如何在bpftrace脚本中使用tracepoint提供的数据,这里可以通过-v选项查看bpftrace可以访问的tracepoint的字段。比如想要跟踪openat系统调用,如果代码中想要获取tracepoint中的一些字段信息做处理,那么就要知道这个tracepoint可以使用哪些字段。那么可以这么操作:
bpftrace -v -l tracepoint:syscalls:sys_enter_openat
tracepoint:syscalls:sys_enter_openat
int __syscall_nr;
int dfd;
const char * filename;
int flags;
umode_t mode;
命令的输出显示了该tracepoint中支持访问的字段,假如我们想要打印每次open文件时的filename,那么示例如下:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat {@[comm]=str(args->filename);} END {print(@); clear(@);}'
执行一段时间后,中断执行后输出:
Attaching 2 probes...
@[awk]: /dev/null
@[cat]: /proc/meminfo
@[dbus-daemon]: /proc/3713504/cmdline
@[grep]: /usr/lib/locale/en_US.utf8/LC_CTYPE
@[lsblk]: device/type
@[sh]: /usr/lib/locale/en_US.utf8/LC_CTYPE
@[sshd]: /etc/selinux/targeted/contexts/openssh_contexts
@[sssd_nss]: /etc/passwd
@[systemd]: /proc/3713493/cgroup
这里需要掌握的就是如何使用字段,在bpftrace脚本中可以直接利用args->filename来引用tracepoint中的filename字段。
systemtap中使用tracepoint
systemtap作为一个trace工具,同样对内核的tracepoint提供了支持,可以用下面的命令来查看可以用的tracepoint点:
stap -L 'kernel.trace("*")'
举例跟踪sched:sched_switch事件,查看-L列出的信息:
kernel.trace("sched:sched_switch") $preempt:bool $prev:struct task_struct* $next:struct task_struct*
这个命令也同样列举除了能够引用的变量:$preempt、 $prev和 $next,这三个变量在systemtap脚本中可以直接引用,例如:
probe kernel.trace("sched:sched_switch")
{
printf("prev pid: %d next pid:%d\n", $prev->pid, $next->pid);
}
如何选择tracepoint
前面介绍了tracepoint怎么用,这只是一个基础,而更重要的是在遇到不同场景下,如何找到合适的tracepoint帮助我们解决问题。问题的根本还是在于对Linux内核的了解,这就需要掌握内核提供的的tracepoint具体作用是什么,在什么情况下会运行到。还是那句话,"Read the fucking source code!"作为抛砖引玉,本文将介绍如何在代码中查找tracepoint位置,后续的深入就得靠个人慢慢修行了。
先来看代码上怎么找tracepoint的定义,现在的内核已经不再直接使用tracepoint的宏定义来实现了,而更倾向于使用TRACE_EVENT,它会对tracepoint做一层封装。直接内核代码中搜索:
grep –rnw TRACE_EVENT
这里只是定义一个tracepoint相关的数据结构,那么放置一个tracepoint到内核代码的某个位置时直接使用:
trace_tracepoint_name
比如定义一个foo_bar:
TRACE_EVENT(foo_bar, ...)
在放置时,就应该使用:
trace_foo_bar(...);
再举个例子,假如想要查找tracepoint:sched_switch的位置:
grep -rn "trace_sched_switch" .
./kernel/sched/core.c:5070: trace_sched_switch(preempt, prev, next);
查看的方法大致类似,通过搜索代码来找到tracepoint的位置,进而掌握在什么情况下可以使用该tracepoint来进行跟踪。这个过程我也同样在持续学习中。