简介
SystemTap是一个诊断Linux系统性能或功能问题的开源软件。它使得对运行时的Linux系统进行诊断调式变得更容易、更简单。有了它,开发者或调试人员不再需要重编译、安装新内核、重启动等烦人的步骤。
我们一般写程序,都会加入相应级别的日志,可以帮助我们定位问题或者观察某些代码路经而不用去使用gdb。但是系统编程,就不能狂打日志(经实验在io路经加日志,rsyslog会经常挂,而且/var/log/meessages里面的信息过多时查找别的心痛错误非常费劲),而且很多调用栈都处于 kernel space,那么普通的调试手段就显得捉襟见肘了。
此时 systemtap 就能派上用场,他会在内核函数加 probe 探针,对 kernel space 函数调用进行统计汇总,甚至可以对其进行干预。但是对 user space 调试支持不是很好。
环境配置
- 我的实验环境是Centos7,内核版本kernel-3.10.0-514.26.2.el7.x86_64,根据版本到官网去下载如下对应的包:
- vault.centos.org
- debuginf.centos.org
[root@xt2 ~]# rpm -qa |grep kernel
kernel-headers-3.10.0-514.16.1.el7.x86_64
kernel-debuginfo-common-x86_64-3.10.0-514.26.2.el7.x86_64
kernel-3.10.0-514.26.2.el7.x86_64
kernel-debuginfo-3.10.0-514.26.2.el7.x86_64
kernel-devel-3.10.0-514.26.2.el7.x86_64
装底下这些kernel包的时候有可能要替换一些已有的包、
kmod-20-9.el7.x86_64.rpm
kmod-libs-20-9.el7.x86_64.rpm
linux-firmware-20160830-49.git7534e19.el7.noarch.rpm
xfsdump-3.1.4-1.el7.x86_64.rpm
xfsprogs-4.5.0-8.el7.x86_64.rpm
把本地老的rpm -e --nodeps xxxx 删掉然后装上面新的
- 安装工具
yum install systemtap
开始编辑stap脚本
一个简单的例子如下:
#!/usr/bin/env stap
global fuck = 0
global g_ino = 0
probe begin {
printf("probe begin\n")
}
probe module("fuse").function("fuse_finish_open")
{
inode = pointer_arg(1)
ino = @cast(inode, "struct inode")->i_ino
g_ino = ino
printf("coming fuse_finish_open ino: %lu\n", ino)
}
probe module("fuse").function("fuse_file_mmap")
{
printf("coming fuse_file_mmap,,,,\n")
}
probe module("fuse").function("fuse_link_write_file")
{
printf("coming fuse_link_write_file\n")
# print_backtrace()
}
probe kernel.function("generic_file_aio_write"){
fuck = 1
nr_segs = ulong_arg(3)
pos = ulong_arg(4)
printf("coming generic_file_aio_write nr_segs: %lu %lu\n", nr_segs, pos)
}
probe kernel.function("page_cache_tree_insert"){
ino = $mapping->host->i_ino
if (ino == g_ino) {
index = $page->index
nrpages = $mapping->nrpages
printf("coming page_cache_tree_insert ino: %lu index:%lu pages: %lu\n", ino, index, nrpages)
}
}
probe kernel.function("page_cache_tree_delete"){
ino = $mapping->host->i_ino
if (ino == g_ino) {
index = $page->index
nrpages = $mapping->nrpages
printf("coming page_cache_tree_delete ino: %lu index:%lu pages: %lu\n", ino, index, nrpages)
#print_backtrace()
}
}
probe kernel.function("__set_page_dirty_nobuffers"){
page = pointer_arg(1)
index = @cast(page, "struct page")->index
printf("coming set_page_dirty_nobuffers: %lu \n", index)
#print_backtrace()
printf("---------------------------------------- page dirty\n")
}
probe module("fuse").function("fuse_release"){
fuck = 1
printf("coming fuse_release****************** \n")
print_backtrace()
}
#probe kernel.function("tag_pages_for_writeback")
#{
# if (fuck == 1) {
# index = ulong_arg(2)
# end = ulong_arg(3)
# printf("coming tag_pages_for_writeback: index %lu end: %lu\n", index, end)
# }
#}
#probe kernel.function("pagevec_lookup_tag"){
# if (fuck == 1) {
# tag = int_arg(4)
# printf("pagevec_lookup_tag %d\n", tag)
# }
#}
#probe kernel.function("pagevec_lookup_tag").return ?
#{
# printf("pagevec_lookup_tag return %u\n", $return)
#print_backtrace()
#}
#probe kernel.function("pagevec_lookup_tag").return{
# printf("find_get_page return %u\n", $$return)
# print_backtrace()
#}
probe module("fuse").function("fuse_writepage"){
page = pointer_arg(1)
index = @cast(page, "struct page")->index
printf("coming fuse_writepage: %lu \n", index)
}
probe module("fuse").function("fuse_writepages"){
printf("coming fuse_writepages:\n")
}
probe module("fuse").function("fuse_send_write_pages"){
printf("coming fuse_send_write_pages:\n")
print_backtrace()
}
#probe kernel.function("write_cache_pages"){
# printf("coming write_cache_pages: \n")
#}
probe module("fuse").function("fuse_writepage_locked"){
page = pointer_arg(1)
index = @cast(page, "struct page")->index
printf("coming fuse_writepage_locked: %lu \n", index)
print_backtrace()
printf("--------------------------------------------------------\n\n\n")
}
probe module("fuse").function("fuse_writepages_fill"){
page = pointer_arg(1)
index = @cast(page, "struct page")->index
printf("coming fuse_writepages_fill: %lu \n", index)
}
probe module("fuse").function("fuse_write_update_size"){
printf("coming fuse_write_update_size: ino %lu size:%lu pos:%lu\n", $inode->i_ino, $inode->i_size, $pos)
}
#probe module("fuse").function("queue_request")
#{
# printf("queue_request ... \n")
#print_backtrace()
#}
#probe kernel.statement("*@mm/page-writeback.c:1941")
#{
# if (fuck == 1) {
#ino = $page->mapping->host->i_ino
# if (ino == g_ino)
# printf("coming lock_page: index %lu \n", $done_index)
# }
#}
#probe module("fuse").function("fuse_flush_dirty"){
# inode = pointer_arg(2)
#ino = @cast(inode, "struct inode", "")->i_ino
# printf("coming fuse_flush_dirty ino: %lu\n", ino)
# printf("coming fuse_flush_dirty \n")
#}
#probe kernel.function("__filemap_fdatawrite_range"){
# mapping = pointer_arg(1)
# ino = @cast(mapping, "struct address_space")->host->i_ino
# printf("coming __filemap_fdatawrite_range ino: %lu\n", ino )
#}
#probe kernel.function("generic_writepages"){
# mapping = pointer_arg(1)
# ino = @cast(mapping, "struct address_space")->host->i_ino
# printf("coming generic_writepages ino: %lu\n", ino)
#}
#probe kernel.function("write_cache_pages"){
# mapping = pointer_arg(1)
# ino = @cast(mapping, "struct address_space")->host->i_ino
# printf("coming write_cache_pages ino: %lu\n", ino)
# printf("--------------------------------------------------------\n")
# print_backtrace()
# printf("--------------------------------------------------------\n\n\n")
#}
#probe module("fuse").function("fuse_writepage_locked"){
# printf("--------------------------------------------------------\n")
# print_backtrace()
# printf("--------------------------------------------------------\n\n\n")
#}
probe end {
printf("probe end")
}
更多脚本可参考(安装完systemtap 就有):/usr/share/systemtap/examples
官网:https://sourceware.org/systemtap/wiki/WarStories
常用方法
1. 列出可以追踪的函数或者变量
查找名字中包含nit的内核函数:
stap -l 'kernel.function("nit")'
查找名字中包含nit的内核函数和变量:
stap -L 'kernel.function("nit")'
自己实验:
列出kernel里面带有write的所有函数
stap -l 'kernel.function("write")' > /tmp/kk
列出fuse里面带有write的所有函数
stap -l 'module("fuse").function("write”)'
2.在脚本里面添加函数,这里面涉及到了如何获取参数:
a.最简单的方式:
我们可以先用
[root@xt1 systemtap]# stap -L 'kernel.function("do_fork")'
kernel.function("do_fork@kernel/fork.c:1685") $clone_flags:long unsigned int $stack_start:long unsigned int $stack_size:long unsigned int $parent_tidptr:int* $child_tidptr:int*
test.stp
--------------------------------------------------------------------------------------------------------
global proc_counter
probe begin {
print("Started monitoring creation of new processes...Press ^C to terminate\n")
printf("%-25s %-10s %-11s %-5s %-s\n", "Process Name", "Process ID", "Clone Flags", "start", "size")
}
probe kernel.function("do_fork") {
proc_counter++
printf("%-25s %-10d 0x%-11x %-5lu %lu\n", execname(), pid(), $clone_flags, $stack_start, $stack_size)
}
probe end {
printf("\n%d processes forked during the observed period\n", proc_counter)
}
上面的函数原型为:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
b. 也可以用第二种方式来获取参数,在缺少debuginfo,即DWARF-less probing的情况下则需要通过uint_arg(),pointer_arg()和ulong_arg()等来获取,这些函数都需要指定当前要获取的参数是第几个参数,编号从1开始。
例如asmlinkage ssize_t sys_read(unsigned int fd, char __user * buf, size_t count)中,uint_arg(1)获取的是fd的值,pointer_arg(2)获取的是buf的地址值,ulong_arg(3)获取的是count参数
c. 如果是通过process.syscall、process("PATH").syscall或者process(PID).syscall来probe系统调用,则可以通过arg1,$arg2等来获取相应的参数值。
3.内联函数或者没有函数可以获取的时候如何获取变量的值
- 内联函数:对于这种函数我们只能够捕捉函数本身有没有进来,但是到底它的参数和返回值我们无法打印。
- 无函数可捕获:有时函数使用宏定义的,我们无法捕获这个函数,但是又必须获取它附近相关的变量的值。
对于上面两种情况,我们可以使出杀手锏“statement”直接定位到源码的某一行来看这个变量!
probe kernel.statement("*@mm/page-writeback.c:1941")
{
if (fuck == 1) {
ino = $page->mapping->host->i_ino
if (ino == g_ino)
printf("coming lock_page: index %lu \n", $done_index)
}
}
4.常用函数
1.execname()
获取当前进程的名称,即可执行文件的名称
2. pid()
获取当前进程的PID
3.pp()
获取当前的probe点。例如 probe process.syscall,process.end { /* scripts */},在块中调用pp()可能会返回"process.syscall"和"process.end"。
4.probefunc()
获取当前probe的函数名称。例如probe sys_read函数,在probe块中调用该函数就会返回sys_read。注意这个函数的返回值是从pp()返回的字符串中解析得到的。
5.tid()
获取当前线程的ID
6.cpu()
获取当前CPU的ID
7.gettimeofday_s()
获取当前Unix时间
8.get_cycles()
获取处理器周期数
9.ppfunc()
获取当前probe的函数名称。在probe指定文件中的函数中时非常有用,可以知道当前的probe位于哪个函数。
10.print_backtrace()
打印内核调用栈信息
11.print_ubacktrace()
打印用户态调用栈信息
12.thread_indent()
输出当前线程的信息,格式为“相对时间 程序名称(线程id):(空格)”,如果当前probe的函数执行的次数约到,空格的数量也就越多。这个函数还有一个参数,用来控制空格的数量。如果参数值越大,则空格的数量越多。相对时间是当前的时间(以微秒为单位)减去指定线程第一次执行thread_indent时的时间。
13.target()
获取当前脚本针对的目标进程ID。需要配置stap的-c或-x命令使用。
4. 技巧
a. "."字符窜连接符
如果想将一个函数返回的字符串和一个常量字符串拼接,则在两者之间加入"."即可,例如probefunc()."123"。
"."运算符还支持".=",即拼接后赋值。
b. 获取stap命令行参数
如果要获取命令行参数准确的值,则使用2....$
c. next操作
如果在probe函数中,发现某个条件没有满足,则结束本次probe的执行,等待下次事件的到来。示例如下:
global i
probe begin {
printf("SystemTap Scripts start.....\n");
}
probe kernel.function("sys_read") {
++i;
if (i % 2) {
next;
}
printf("i = %d\n", i);
}
d. $$vars
如果合适的话,可以通过$$vars获取当前所有可见的变量列表。如果列表中某些成员的值显示"?",则表示当前这些变量尚未初始化,还不能访问。
e、call和inline后缀的区别
如果加上call后缀,只有在当前probe的函数是非内联函数时才会触发事件。例如,如果在内联函数pskb_may_pull()的probe点加上call后缀,则事件不会被触发。对于非内联函数的probe点,不能加上inline后缀,否则编译时会报错。如果想触发内联函数的probe事件,一定不能加上call后缀。如果call和inline后缀都不加,则内核函数和非内联函数的probe事件都会触发。
f、输出"%"字符
在systemtap中使用转义字符来输出"%"没有效果,在编译时会报错,可以使用"%%"来输出"%”。
参考:
1.https://blog.csdn.net/zhangskd/article/details/25708441
2.https://www.ibm.com/developerworks/cn/linux/l-cn-systemtap3/
3.https://www.jianshu.com/p/84b3885aa8cb