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的基本使用方法及我们调试内核可能用到的方法进行总结归纳。
Systemtap可以通过指令执行,也通过脚本执行。通过指令执行使用'-e'选项,例如:
stap -e 'probe vfs.read {printf("read performed\n"); exit()}'
其中,【''】内为执行的内容,probe vfs.read为event, {printf("read performed\n"); exit()}为handler。即当event发生时,执行handler的内容。
上例可写成脚本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)
语句 说明
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的一些函数:这些函数可以作为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
显示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的大小。
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