当前位置: 首页 > 工具软件 > SystemTap > 使用案例 >

systemtap工具使用介绍

谭刚毅
2023-12-01

简介

       systemtap 是利用Kprobe 提供的API来实现动态地监控和跟踪运行中的Linux内核的工具,相比Kprobe,systemtap更加简单,提供给用户简单的命令行接口,以及编写内核指令的脚本语言。对于开发人员,systemtap是一款难得的工具内核调试工具。

安装说明

首先确定内核版本:

uname  -r: 3.10.0-123.el7.x86_64

对应于我们使用的内核版本,需要安装以下安装包:

kernel-debug-debuginfo-3.10.0-123.el7.x86_64.rpm

kernel-debug-devel-3.10.0-123.el7.x86_64.rpm

kernel-debuginfo-3.10.0-123.el7.x86_64.rpm

kernel-debuginfo-common-x86_64-3.10.0-123.el7.x86_64.rpm

kernel-headers-3.10.0-123.el7.x86_64.rpm

将上述安装包拷贝到本地虚机,执行一下命令rpm -ivh kernel-debuginfo-common-x86_64-3.10.0-862.el7.x86_64.rpm

安装上述安装包之后,systemtap便可以使用。

执行一下命令检测systemtap是否可以使用:

stap -e 'probe vfs.read {printf("read performed\n"); exit()}'

 输出结果

下面简单说下5个pass的作用:

Pass 1 - parse:这个阶段主要是检查输入脚本是否存在语法错误,例如大括号是否匹配,变量定义是否规范等

Pass 2 - elaborate:这个阶段主要是对输入脚本中定义的探测点或者用到的函数展开,不但需要综合SystemTap的预定义脚本库,还需要分析内核或者内核模块的调试信息

Pass 3 -translate: 在这个阶段,将展开后的脚本转换成C文件。前三个阶段的功能类似于编译器,将.stp文件编译成为完整的.c文件,因此又被合起来称为转换器(translator)

Pass 4 - build:在这个阶段,将C源文件编译成内核模块,在这过程中还会用到SystemTap的运行时库函数。

Pass 5 - run:这个阶段,将编译好的内核模块插入内核,开始进行数据收集和传输。

NOTE:

1、我们linux系统中默认安装systetap,上述安装的是一些debug信息的安装包;

2、不同的内核版本需要对应与之相对应的debug信息安装包,例如kernel-debug-devel-XXXX.rpm,其中'XXXX'对应相应的内核版本号。

使用说明

本章旨在对systemtap使用手册中的systemtap的基本使用方法及我们调试内核可能用到的方法进行总结归纳。

1、stap指令执行方式

Systemtap可以通过指令执行,也通过脚本执行。通过指令执行使用'-e'选项,例如:

stap -e 'probe vfs.read {printf("read performed\n"); exit()}'

       其中,【''】内为执行的内容,probe vfs.read为event, {printf("read performed\n"); exit()}为handler。即当event发生时,执行handler的内容。

2、Systemtap可以通过脚本执行

上例可写成脚本test.stp如下:

probe vfs.read

{

       printf("read performed\n");

exit();

}

运行时,执行stap test.stp即可。

探针模式例子

探针类型                                                                        说明

begin                                                                        在脚本开始时触发

end                                                                                   在脚本结束时触发

kernel.function("sys_sync")                                      调用sys_sync时触发

kernel.function("sys_sync").call                                同上

kernel.function("sys_sync").return                            返回sys_sync时触发

kernel.syscall.*                                                          进行任何系统调用时触发

kernel.function("*@kernel/fork.c:934")                    到达 fork.c的第 934行时触发

module("ext3").function("ext3_file_write")               调用 ext3write函数时触发

timer.jiffies(1000)                                                      每隔 1000个内核 jiffy触发一次

timer.ms(200).randomize(50)                                    每隔 200毫秒触发一次,带有线性分布的随机附加时间(-50到 +50)

SystemTap 的语言元素

语句                                                                  说明

if (exp) {} else {}                                       标准的if-then-else语句

for (exp1 ; exp2 ; exp3 ) {}                        一个for循环

while (exp) {}                                            标准的while循环

do {} while (exp)                                       一个do-while循环

break                                                        退出迭代

continue                                                    继续迭代

next                                                          从探针返回

return                                                       从函数返回一个表达式

foreach (VAR in ARRAY) {}                      迭代一个数组,将当前的键分配给VAR

SystemTap的一些函数

SystemTap的一些函数:这些函数可以作为printf()的参数,
execname()是调用被监控的内核函数或是系统调用的那个进程,
pid()就是当前进程ID,
tid()是当前线程ID,
uid()是当前用户ID,
cpu()是当前CPU号,
gettimeofday_s()返回1970年以来的秒数,
ctime()将秒数转换成日期,
pp()描述当前被执行的探针的字符串,
thread_indent()缩进,
name就是指定的系统调用,
target()在指定要跟踪的进程或命令时用stap script -x process ID或者stap script -c command然后再脚本中就可以使用target()来获取指定的目标process ID比如在脚本中可以判断if (pid() == target()) 再进行处理。


变量:
变量可以在一个handler中任意使用,声明一个名字如global count,然后就可以从一个函数或者表达式中赋值,在一个表达式中使用它。SystemTap自动识别一个变量的类型是字符串或是整型。默认的变量只能在本地probe使用。要想在probes之间共享变量,可以在probe之外将变量声明为global。
目标变量:
获取代码中可见的变量的值,比如可以使用stap -L 'kernel.function("vfs_read")'来列出vfs_read函数可用的目标变量,输出类似于:
kernel.function("vfs_read@fs/read_write.c:277") $file:struct file* $buf:char* $count:size_t $pos:loff_t* 
每个目标变量由$开头,变量的类型跟随在:后面。
当一个目标变量不是一个本地变量,而是一个全局外部变量或是一个文件中的static变量定义在另一个文件中,它就可以通过"@var("varname@src/file.c")"来引用。而且还可以通过->来进一步引用结构体变量的成员。如:@var("files_stat@fs/file_table.c")->max_files);

一些脚本例子:

监测内核函数

每当内核函数do_fork()被调用时,显示调用它的进程名、进程ID、函数参数

#!/usr/bin/stap

global proc_counter

probe begin

{

       print("Started monitoring creation of new processes...Press ^C to terminate\n")

       printf("%-25s %-10s %-s\n","Process Name","Process ID","Clone Flags")

}

probe kernel.function("do_fork")

{        

               proc_counter++        

               printf("%-25s %-10d 0x%-x\n", execname(), pid(), $clone_flags)

}

probe end

{        

       printf("\n%d processes forked during the observed period\n", proc_counter)

}

执行结果:

Started monitoring creation of new processes...Press ^C to terminate

Process Name              Process ID Clone Flags

kthreadd                  2          0x800711

ksmtuned                  577        0x1200011

ksmtuned                  1828       0x1200011

ksmtuned                  577        0x1200011

ksmtuned                  1830       0x1200011

ksmtuned                  1831       0x1200011

ksmtuned                  1830       0x1200011

ksmtuned                  1830       0x1200011

ksmtuned                  577        0x1200011

^C

9 processes forked during the observed period

打印4s内所有open系统调用的信息

显示4秒内open系统调用的信息:调用进程名、进程ID、函数参数。

#!/usr/bin/stap

probe begin

{

       log("begin to probe")

}

probe syscall.open

{

       printf("%s(%d) open (%s)\n", execname(), pid(), argstr)

}

probe timer.ms(4000) #after 4 seconds

{

       exit()

}

probe end

{

       log("end to probe")

}

输出结果:

begin to probe

systemd(1) open ("/proc/1019/cgroup", O_RDONLY|O_CLOEXEC)

systemd-udevd(368) open ("/sys/fs/cgroup/systemd/system.slice/systemd-udevd.service/cgroup.procs", O_RDONLY|O_CLOEXEC)

end to probe

监测源文件中所有函数入口和出口

括号内的探测点描述包含三个部分:

function name part:函数名

@file name part:文件名

function line part:所在行号

例如:

probe kernel.function("*@net/socket.c") {}  

probe kernel.function("*@net/socket.c").return {}  

这里指定函数名为任意(用*表示),指定文件名为net/socket.c,探测函数的入口和返回。

还可以用":行号"来指定行号。

查找匹配的内核函数和变量

查找名字中包含nit的内核函数:

stap -l 'kernel.function("*nit*")'

查找名字中包含nit的内核函数和变量:

stap -L 'kernel.function("*nit*")'

监控所有进程的收发包情况(使用聚合步骤数字数据)

聚合实例时捕捉数字值的统计数据的出色方法。当您捕捉大量数据时,这个方法非常高效有用。在这个例子中,您收集关于网络包接收和发送的数据。清单 8定义两个新的探针来捕捉网络 I/O。每个探针捕捉特定网络设备名、PID和进程名的包长度。在用户按 Ctrl-C调用的 end探针提供发送捕获的数据的方式。在本例中,您将遍历recv聚合的内容、为每个元组(设备名、PID和进程名)相加包的长度,然后发出该数据。注意,这里使用提取器来相加元组:@count提取器获取捕获到的长度(包计数)。您还可以使用@sum提取器来执行相加操作,分别使用@min或@max来收集最短或最长的程度,以及使用@avg来计算平均值。

#!/usr/bin/stap

global recv, xmit  

 

probe begin {  

   printf("Starting network capture...Press ^C to terminate\n")  

}  

 

probe netdev.receive {  

   recv[dev_name, pid(), execname()] <<< length  

}  

 

probe netdev.transmit {  

   xmit[dev_name, pid(), execname()] <<< length  

}  

 

probe end {  

   printf("\nCapture terminated\n\n")  

   printf("%-5s %-15s %-10s %-10s %-10s\n",  

       "If", "Process", "Pid", "RcvPktCnt", "XmtPktCnt")  

     

   foreach([dev, pid, name] in recv) {  

       recvcnt = @count(recv[dev, pid, name])  

       xmtcnt =  @count(xmit[dev, pid, name])  

       printf("%-5s %-15s %-10d %-10d %-10d\n", dev, name, pid, recvcnt, xmtcnt)  

   }  

}  

输出内容

Starting network capture...Press ^C to terminate

^C

Capture terminated

If    Process         Pid        RcvPktCnt  XmtPktCnt

eth0  swapper/0       0          169        51        

eth0  vmtoolsd        507        1          1        

eth0  ksoftirqd/0     3          1          1

捕获柱状图数据

最后一个例子展示 SystemTap用其他形式呈现数据有多么简单 --在本例中以柱状图的形式显示数据。返回到是一个例子中,将数据捕获到一个名为histogram的聚合中(见清单 10)。然后,使用netdev接收和发送探针以捕捉包长度数据。当探针结束时,您将使用@hist_log提取器以柱状图的形式呈现数据。

#!/usr/bin/stap

global histogram


probe begin {

         printf("Capturing...\n")

}


probe netdev.receive {

         histogram <<< length

}


probe netdev.transmit {

         histogram <<< length

}


probe end {

         printf( "\n" )

         print( @hist_log(histogram) )

}

输出结果:

value |------------------------------------------------------------------------------------------------------------------------------ count

   8 |                                                                                                                                                                                        0

  16 |                                                                                                                                                                                       0

  32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  3251

  64 |@@                                                                                                                                                                              190

 128 |                                                                                                                                                                                      10

 256 |                                                                                                                                                                                      64

在这个例子中,使用了一个浏览器会话、一个 FTP会话和ping来生成网络流量。@hist_log提取器是一个以 2 为底数的对数柱状图(如下所示)。还可以步骤其他柱状图,从而使您能够定义 bucket的大小。

打印出在过去的5秒钟里调用次数最多的那些系统调用

stap -ve'global syscalls function print_top () { cnt=0 log("SYSCALL\t\t\t\tCOUNT") foreach ([name] in syscalls-) {printf("%-20s\t\t%5d\n",name, syscalls[name]) if (cnt++ == 10) break} printf("--------------------------------------\n") delete syscalls} probe syscall.* { syscalls[probefunc()]++ } probe timer.ms(5000) { print_top() }'

输出结果

SYSCALL                                COUNT

sys_times                             102

SyS_poll                               54

SyS_read                               37

sys_ppoll                              25

sys_select                             20

SyS_semtimedop                         20

sys_restart_syscall                    16

SyS_epoll_wait                         13

SyS_newstat                            11

sys_wait4                              10

sys_close                               8

--------------------------------------

SYSCALL                                COUNT

sys_close                             295

SyS_read                              291

sys_open                              269

SyS_newstat                           163

SyS_rt_sigprocmask                    119

sys_times                             112

SyS_rt_sigaction                       87

SyS_poll                               56

sys_mmap_pgoff                         53

SyS_newfstat                           33

SyS_mprotect                           28

--------------------------------------

监控所有系统调用

包含一个全局变量定义和 3个独立的探针。在首次加载脚本时调用第一个探针(begin探针)。在这个探针中,您可以发出一条表示脚本在内核中运行的文本消息。接下来是一个syscall探针。注意这里使用的通配符 (*),它告诉 SystemTap监控所有匹配的系统调用。当该探针触发时,将为特定的 PID和进程名增加一个关联数组元素。最后一个探针是 timer探针。这个探针在 10,000毫秒(10秒)之后触发。与这个探针相关联的脚本将发送收集到的数据(遍历每个关联数组成员)。当遍历了所有成员之后,将调用exit调用,这导致卸载模块和退出所有相关的 SystemTap进程。

#!/usr/bin/stap

global syscalllist


probe begin

{

         printf("System Call Monitoring Started (10 seconds)...\n")

}


probe syscall.*

{

         syscalllist[pid(), execname()]++

}


probe timer.ms(10000)

{

         foreach ( [pid, procname] in syscalllist ) {

             printf("%s[%d] = %d\n", procname, pid, syscalllist[pid, procname] )

         }

   exit()

}

打印函数调用堆栈。

       一般打印函数调用堆栈,用户进程的话直接gdb上去在文件行数或者函数设置一个断点,等待断点停下来后,用gdb命令backtrace(缩写bt)就能得到调用堆栈了,这样很有用,不管是学习新代码或者debug时都非常有用,比如在学nginx的时候,里面很多函数指针,用source insight、understand、vim+ctag都很难把代码流程读通,还是让代码运行起来后gdb上去设置断点bt一下就知道了。用户进程这种方法很管用,但在内核就不好办了,内核也一堆函数指针,很不好分析函数的调用流程,还好SystemTap给我们提供了print_backtrace,比如我们要看内核是怎么收包的,它的调用流程是怎样的,我们在netif_receive_skb函数上设置个探测点再把调用函数堆栈打印出来就知道了:

root@jusse ~/systemtap# cat netif_receive_skb.stp

probe kernel.function("netif_receive_skb")

{

   printf("--------------------------------------------------------\n");

   print_backtrace();

   printf("--------------------------------------------------------\n");

}

root@jusse ~/systemtap# stap netif_receive_skb.stp

--------------------------------------------------------

0xffffffff8164dc00 : netif_receive_skb+0x0/0x90 [kernel]

0xffffffff8164e280 : napi_gro_receive+0xb0/0x130 [kernel]

0xffffffff81554537 : handle_incoming_queue+0xe7/0x100 [kernel]

0xffffffff815555d9 : xennet_poll+0x279/0x430 [kernel]

0xffffffff8164ee09 : net_rx_action+0x139/0x250 [kernel]

0xffffffff810702cd : __do_softirq+0xdd/0x300 [kernel]

0xffffffff8107088e : irq_exit+0x11e/0x140 [kernel]

0xffffffff8144e785 : xen_evtchn_do_upcall+0x35/0x50 [kernel]

0xffffffff8176c9ed : xen_hvm_callback_vector+0x6d/0x80 [kernel]

--------------------------------------------------------

参考链接

使用systemtap调试新增模块的例子:http://blog.chinaunix.net/uid-26000137-id-5088903.html

SystemTap使用技巧:http://blog.csdn.net/wangzuxi/article/details/42849053

 类似资料: