在大多数情况下,SystemTap脚本是每个SystemTap会话的基石。SystemTap脚本决定了需要收集的信息类型,也决定了对收集到的信息的处理方式。
在本章的开头曾经提到过,SystemTap脚本由两部分组成:事件和处理程序。一旦SystemTap会话准备就绪,SystemTap会监控操作系统中特定的事件,并在事件发生的时候触发对应的处理程序。
一个事件和它对应的处理程序合称探针。一个SystemTap脚本可以有多个探针。 一个探针的处理程序部分通常称之为探针主体(probe body)
以应用开发的方式类比,使用事件和处理程序就像在程序的特定位置插入打日志的语句。每当程序运行时,这些日志会帮助你查看程序执行的流程。
SystemTasp脚本允许你在无需重新编译代码,即可插入检测指令,而且处理程序也不限于单纯地打印数据。事件会触发对应的处理程序;对应的处理程序记录下感兴趣的数据,并以你指定的格式输出。
SystemTap脚本的后缀是.stp,并以这样的语句表示一个探针:
.stp
probe event {statements}
译注:如果你写过awk脚本,应该会感觉似曾相识。
SystemTap支持给一个探针指定多个事件;每个事件以逗号隔开。如果给某一个探针指定了多个事件,只要其中一个事件发生,SystemTap就会执行对应的处理程序。
每个探针有自己对应的语句块。语句块由花括号({})括住,包含事件发生时需要执行的所有语句。SystemTap会顺序执行这些语句;语句间通常不需要特殊的分隔符或终止符。
{}
SystemTap脚本的语句块使用跟C语言一样的语法。语句块内允许嵌套。
SystemTap允许你编写函数来提取探针间公共的逻辑。所以,与其在多个探针间复制粘贴重复的语句,你不如把它们放入函数中,就像:
function function_name(arguments) {statements} probe event {function_name(arguments)}
当探针被触发时,function_name中的语句会被执行。arguments是传递给函数的可选的入参。
function_name
arguments
本节仅仅是粗略地介绍下SystemTap脚本的结构。要想了解更详细的内容,最好坚持读到第5章,SystemTap脚本集锦;其中的每一节都会详细介绍一个脚本,包含它所监控的事件、它的处理程序和输出内容。
SystemTap事件大致分为两类:同步事件和异步事件。
同步事件会在任意进程执行到内核特定位置时触发。你可以用它来作为其它事件的参照点,毕竟同步事件有着清晰的上下文信息。
同步事件包括:
syscall.system_call
进入名为system_call的系统调用。如果想要监控的是退出某个系统调用的事件,在后面添加.return。举个例子,要想监控进入和退出系统调用close的事件,应该使用syscall.close和syscall.close.return。
system_call
.return
close
syscall.close
syscall.close.return
vfs.file_operation
进入虚拟文件系统(VFS)名为file_operation的文件操作。跟系统调用事件一样,在后面添加.return可以监控对应的退出事件。 译注:file_operation取值的范畴,取决于当前内核中struct file_operations的定义的操作(可能位于include/linux/fs.h中,版本不同位置会不一样,建议上http://lxr.free-electrons.com/ident 查找file_operations)。
file_operation
struct file_operations
include/linux/fs.h
file_operations
kernel.function("function")
进入名为function的内核函数。举个例子,kernel.function("sys_open")即内核函数sys_open被调用时所触发的事件。同样,kernel.function("sys_open").return会在sys_open函数调用返回时被触发。
function
kernel.function("sys_open")
sys_open
kernel.function("sys_open").return
在定义探测事件时,可以使用像*这样的通配符。你也可以用内核源码文件名限定要跟踪的函数。看下面的例子:
*
probe kernel.function("*@net/socket.c") { } probe kernel.function("*@net/socket.c").return { }
在上面的例子中,第一个探针会监控net/socket.c中的所有函数的调用。第二个会监控所有这些函数的退出。注意在这个例子里,处理程序是空的;所以,即使事件被触发了,什么也不会发生。 译注:例子中用的是探测内核源码中的函数的语法。完整的语法是func_name@file_name[:line_num],由函数名、文件名、行号三部分组成。其中函数名在例子中为*,匹配任意函数。行号是可选的,在上面的例子里就被忽略掉了。如果想指定某个范围内的函数,如从行x到y,使用:x-y这样格式作为行号。
net/socket.c
func_name@file_name[:line_num]
:x-y
kernel.trace("tracepoint")
到达名为tracepoint的静态内核探测点(tracepoint)。较新的内核(>= 2.6.30)包含了特定事件的检测代码。这些事件一般会被标记成静态内核探测点。一个例子是,kernel.trace("kfree_skb")表示内核释放了一个网络缓冲区的事件。(译注:想知道当前内核设置了哪些静态内核探测点吗?你需要运行sudo perf list。)
tracepoint
kernel.trace("kfree_skb")
sudo perf list
module("module").function("function")
进入指定模块module的函数function。举个例子:
module
probe module("ext3").function("*") { } probe module("ext3").function("*").return { }
上面例子的第一个探针,会在每个ext3模块中的函数被调用时触发。第二个探针会在函数退出时触发。一切就跟kernel.function()一样。
kernel.function()
系统内的所有内核模块通常都在/lib/modules/kernel_version,其中kernel_version取当前内核版本号。模块的后缀名为.ko。 (译注:在该路径下使用find -name '*.ko' -printf '%f\n' | sed 's/\.ko$//'可列出所有的内核模块)
/lib/modules/kernel_version
kernel_version
.ko
find -name '*.ko' -printf '%f\n' | sed 's/\.ko$//'
异步事件跟特定的指令或代码的位置无关。 这部分事件主要包含计数器、定时器和其它类似的东西。
begin
SystemTap会话的启动事件,会在脚本开始时触发。
end
SystemTap会话的结束事件,会在脚本结束时触发。
timer events
用于周期性执行某段处理程序。举个例子:
probe timer.s(4) { printf("hello world\n") }
上面的例子中,每隔4秒就会输出hello world。还可以使用其它规格的定时器:
hello world
timer.ms(milliseconds) timer.us(microseconds) timer.ns(nanoseconds) timer.hz(hertz) timer.jiffies(jiffies)
定时事件总是跟其它事件搭配使用。其它事件负责收集信息,而定时事件定期输出当前状况,让你看到数据随时间的变化情况。
限于篇幅,还有些SystemTap事件就不再一一介绍了。如果你想了解更多内容,请man stapprobes。该man page中的SEE ALSO一节,包括了通往其它man page的链接,你还可以随之找到某些特定子系统和组件所支持的事件。
man stapprobes
SEE ALSO
看一下下面的示例脚本:
probe begin { printf ("hello world\n") exit () }
在上面的例子中,每当会话开始时,begin事件会触发{}内的处理程序,输出hello world加一个换行符,然后退出。
SystemTap脚本会一直运行,直到执行了exit()函数。如果你想中途退出一个脚本,可以用Ctrl+c中断。
exit()
Ctrl+c
printf
printf()是最简单的SystemTap函数之一,可以跟许多函数搭配使用,用来输出数据。通常我们会这样调用printf():
printf()
printf ("format string\n", arguments)
format string指明arguments输出的格式。在前面的例子里,printf语句内没有指定format格式符。在格式字符串(format string)中,你可以用%s表示字符串,%d表示数字。格式字符串中可以包含多个格式符,每个格式符对应一个参数;每个参数之间用逗号隔开。
format string
%s
%d
SystemTap的printf语句跟C的printf语句,无论在语法还是在格式字符串上都差不多。
下面让我们再看多一个例子:
probe syscall.open { printf ("%s(%d) open\n", execname(), pid()) }
在上面的例子中,SystemTap会在每次open被调用时,输出调用程序的名字和PID,外加open这个词。该探针输出的结果看上去会是这样:
open
vmware-guestd(2206) open hald(2360) open hald(2360) open hald(2360) open df(3433) open df(3433) open df(3433) open hald(2360) open
你可以在printf()里使用其他的SystemTap函数。比如上面的例子中就用到execname()(获取触发事件的进程名)和pid()(当前进程ID)。
execname()
pid()
下面列出常用的SystemTap函数:
tid()
当前的tid(thread id)。
uid()
当前的uid。
cpu()
当前的CPU号
gettimeofday_s()
自epoch以来的秒数
ctime()
将上一个函数返回的秒数转化成时间字符串
pp()
返回描述当前处理的探测点的字符串
thread_indent()
你可以用这个函数来组织你的输出结果。这个函数接受一个表示缩进差额的参数,用来更新当前线程的“缩进计数器”(其实就是用于缩进的空格数)。它返回的是加了足够缩进的标识字符串。 这个标识字符串包括一个时间戳(表示自从该线程首次调用thread_indent()以来所经过的毫秒数),一个进程名,一个tid。由此可以清晰地看出函数的调用次序和调用层级,和每次调用时的间隔。 如果一个函数调用后随即退出,很容易就能看出被触发的两个事件是相关的。然而,在大多数情况下,一个函数调用和退出之间,往往会有调用其他别的函数。通过缩进,可以相对更清晰地看出某个函数调用和退出的时机。
看一下下面使用thread_indent()的例子:
probe kernel.function("*@net/socket.c").call { printf ("%s -> %s\n", thread_indent(1), probefunc()) } probe kernel.function("*@net/socket.c").return { printf ("%s <- %s\n", thread_indent(-1), probefunc()) }
它输出的结果大概是这个样子的,注意箭头前面的空格数:
0 ftp(7223): -> sys_socketcall 1159 ftp(7223): -> sys_socket 2173 ftp(7223): -> __sock_create 2286 ftp(7223): -> sock_alloc_inode 2737 ftp(7223): <- sock_alloc_inode 3349 ftp(7223): -> sock_alloc 3389 ftp(7223): <- sock_alloc 3417 ftp(7223): <- __sock_create 4117 ftp(7223): -> sock_create 4160 ftp(7223): <- sock_create 4301 ftp(7223): -> sock_map_fd 4644 ftp(7223): -> sock_map_file 4699 ftp(7223): <- sock_map_file 4715 ftp(7223): <- sock_map_fd 4732 ftp(7223): <- sys_socket 4775 ftp(7223): <- sys_socketcall
上面的输出包含如下信息:
->
<-
name
返回系统调用的名字。这个变量只能在syscall.system_call触发的处理程序中使用。
target()
当你通过stap script -x PID或stap script -c command来执行某个脚本script时,target()会返回你指定的PID或命令名。举个例子:
stap script -x PID
stap script -c command
script
probe syscall.* { if (pid() == target()) printf("%s\n", name) }
当上面的例子中的脚本带命令行参数-x PID运行时,它会监控所有的系统调用(syscall.*),并输出其中由指定进程所触发的系统调用。 你当然可以把上面例子中的target()替换成你想要指定的PID。不过使用target()让你的脚本可以重用。现在你只需在运行时指定PID,而无需每次都修改掉硬编码的PID值。
-x PID
syscall.*
要想了解更多关于SystemTap函数的信息,请man stapfuncs。
man stapfuncs
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8