Systemtap的运行过程如图所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。
-w 关闭警告信息。
-W 将警告信息变成错误信息打印。
-b 让内核到用户数据传输使用bulk模式。使用RelayFS文档系统来将数据从内核空间传输到用户空间;
-c CMD 开始探测,运行CMD,当CMD完成时退出。
-x PID 设置target()
-v 打印中间信息
-g 采用guru模式,允许脚本中嵌入C语句
Systemtap使用
探针
SystemTap 脚本由探针和在触发探针时需要执行的代码块组成。探针有许多预定义模式,表 1列出了其中的一部分。这个表列举了几种探针类型,包括调用内核函数和从内核函数返回。
表 1.探针模式例子
探针类型 |
说明 |
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") |
调用 ext3 write函数时触发 |
timer.jiffies(1000) |
每隔 1000 个内核 jiffy触发一次 |
timer.ms(200).randomize(50) |
每隔 200 毫秒触发一次,带有线性分布的随机附加时间(-50到 +50) |
我们通过一个简单的例子来理解如何构造探针,并将代码与该探针相关联。清单 3显示了一个样例探针,它在调用内核系统调用sys_sync时触发。当该探针触发时,您希望计算调用的次数,并发送这个计数以及表示调用进程 ID(PID)的信息。首先,声明一个任何探针都可以使用的全局值(全局名称空间对所有探针都是通用的),然后将它初始化为 0。其次,定义您的探针,它是一个探测内核函数sys_sync的条目。与探针相关联的脚本将递增count变量,然后发出一条消息,该消息定义调用的次数和当前调用的 PID。注意,这个例子与C语言中的探针非常相似(探针定义语法除外),如果具有C语言背景将非常有帮助。
清单 3.一个简单的探针和脚本
global count=0
probe kernel.function("sys_sync") { count++ printf( "sys_sync called %d times, currently by pid %d/n", count, pid ); } |
您还可以声明探针可以调用的函数,尤其是希望供多个探针调用的通用函数。这个工具还支持递归到给定深度。
变量和类型
SystemTap 允许定义多种类型的变量,但类型是从上下文推断得出的,因此不需要使用类型声明。在 SystemTap中,您可以找到数字(64位签名的整数)、整数(64 位)、字符串和字面量(字符串或整数)。您还可以使用关联数组和统计数据(我们稍后讨论)。
表达式
SystemTap 提供C语言中常用的所有必要操作符,并且用法也是一样的。您还可以找到算术操作符、二进制操作符、赋值操作符和指针废弃。您还看到从C语言带来的简化,其中包括字符串连接、关联数组元素和合并操作符。
语言元素
在探针内部,SystemTap 提供一组类似于C一样易于使用的语句。注意,尽管该语言允许您开发复杂的脚本,但每个探针只能执行 1000条语句(这个数量是可配置的)。表 2列出了一小部分语句作为例子。注意,在这里的许多元素和C中的一样,尽管有一些附加的东西是特定于 SystemTap的。
表 2. 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 |
本文在样例脚本中探索了统计数据和聚合功能,因为这是C语言中不存在的。
最后,SystemTap 提供许多内部函数,这些函数提供关于当前上下文的额外信息。例如,您可以使用caller()识别当前的调用函数,使用cpu()识别当前的处理器号码,以及使用pid()返回 PID。SystemTap 还提供许多其他函数,提供对调用堆栈和当前注册表的访问。
SystemTap 例子
在简单介绍了 SystemTap 的要点之后,我们接下来通过一些简单的例子来了解 SystemTap的工作原理。本文还展示了该脚本语言的一些有趣方面,比如聚合。
系统调用监控
前一个小节探索了一个监控sync系统调用的简单脚本。现在,我们查看一个更加具有代表性的脚本,它可以监控所有系统调用并收集与它们相关的额外信息。
清单 4 显示的简单脚本包含一个全局变量定义和 3个独立的探针。在首次加载脚本时调用第一个探针(begin探针)。在这个探针中,您可以发出一条表示脚本在内核中运行的文本消息。接下来是一个syscall探针。注意这里使用的通配符 (*),它告诉 SystemTap监控所有匹配的系统调用。当该探针触发时,将为特定的 PID和进程名增加一个关联数组元素。最后一个探针是 timer探针。这个探针在 10,000毫秒(10 秒)之后触发。与这个探针相关联的脚本将发送收集到的数据(遍历每个关联数组成员)。当遍历了所有成员之后,将调用exit调用,这导致卸载模块和退出所有相关的 SystemTap 进程。
清单 4.监控所有系统调用 (profile.stp)
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() } |
清单 4 中的脚本的输出如清单 5所示。从这个脚本中您可以看到运行在用户空间中的每个进程,以及在 10秒钟内发出的系统调用的数量。
清单 5. profile.stp脚本的输出
$ sudo stap profile.stp System Call Monitoring Started (10 seconds)... stapio[16208] = 104 gnome-terminal[6416] = 196 Xorg[5525] = 90 vmware-guestd[5307] = 764 hald-addon-stor[4969] = 30 hald-addon-stor[4988] = 15 update-notifier[6204] = 10 munin-node[5925] = 5 gnome-panel[6190] = 33 ntpd[5830] = 20 pulseaudio[6152] = 25 miniserv.pl[5859] = 10 syslogd[4513] = 5 gnome-power-man[6215] = 4 gconfd-2[6157] = 5 hald[4877] = 3 $ |
特定的进程的系统调用监控
在这个例子中,您稍微修改了上一个脚本,让它收集一个进程的系统调用数据。此外,除了仅捕捉计数之外,还捕捉针对目标进程的特定系统调用。清单 6显示了该脚本。
这个例子根据特定的进程进行了测试(在本例中为syslog守护进程),然后更改关联数组以将系统调用名映射到计数数据。
清单 6.新系统调用监控脚本 (syslog_profile.stp)
global syscalllist
probe begin { printf("Syslog Monitoring Started (10 seconds).../n") }
probe syscall.* { if (execname() == "syslogd") { syscalllist[name]++ } }
probe timer.ms(10000) { foreach ( name in syscalllist ) { printf("%s = %d/n", name, syscalllist[name] ) } exit() } |
清单 7 提供了该脚本的输出。
清单 7.新脚本的 SystemTap 输出 (syslog_profile.stp)
$ sudo stap syslog_profile.stp Syslog Monitoring Started (10 seconds)... writev = 3 rt_sigprocmask = 1 select = 1 $ |
使用聚合步骤数字数据
聚合实例时捕捉数字值的统计数据的出色方法。当您捕捉大量数据时,这个方法非常高效有用。在这个例子中,您收集关于网络包接收和发送的数据。清单 8定义两个新的探针来捕捉网络 I/O。每个探针捕捉特定网络设备名、PID和进程名的包长度。在用户按 Ctrl-C调用的 end 探针提供发送捕获的数据的方式。在本例中,您将遍历recv聚合的内容、为每个元组(设备名、PID和进程名)相加包的长度,然后发出该数据。注意,这里使用提取器来相加元组:@count提取器获取捕获到的长度(包计数)。您还可以使用@sum提取器来执行相加操作,分别使用@min或@max来收集最短或最长的程度,以及使用@avg来计算平均值。
清单 8.收集网络包长度数据 (net.stp)
global recv, xmit
probe begin { printf("Starting network capture (Ctl-C to end)/n") }
probe netdev.receive { recv[dev_name, pid(), execname()] <<< length }
probe netdev.transmit { xmit[dev_name, pid(), execname()] <<< length }
probe end { printf("/nEnd Capture/n/n")
printf("Iface Process........ PID.. RcvPktCnt XmtPktCnt/n")
foreach ([dev, pid, name] in recv) { recvcount = @count(recv[dev, pid, name]) xmitcount = @count(xmit[dev, pid, name]) printf( "%5s %-15s %-5d %9d %9d/n", dev, name, pid, recvcount, xmitcount ) }
delete recv delete xmit } |
清单 9 提供了清单 8中的脚本的输出。注意,当用户按 Ctrl-C时退出脚本,然后发送捕获的数据。
清单 9. net.stp的输出
$ sudo stap net.stp Starting network capture (Ctl-C to end) ^C End Capture
Iface Process........ PID.. RcvPktCnt XmtPktCnt eth0 swapper 0 122 85 eth0 metacity 6171 4 2 eth0 gconfd-2 6157 5 1 eth0 firefox 21424 48 98 eth0 Xorg 5525 36 21 eth0 bash 22860 1 0 eth0 vmware-guestd 5307 1 1 eth0 gnome-screensav 6244 6 3 Pass 5: run completed in 0usr/50sys/37694real ms. $ |
捕获柱状图数据
最后一个例子展示 SystemTap 用其他形式呈现数据有多么简单 —— 在本例中以柱状图的形式显示数据。返回到是一个例子中,将数据捕获到一个名为histogram 的聚合中(见清单 10)。然后,使用netdev接收和发送探针以捕捉包长度数据。当探针结束时,您将使用@hist_log提取器以柱状图的形式呈现数据。
清单 10.步骤和呈现柱状图数据 (nethist.stp)
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) ) } |
清单 11 显示了清单 10的脚本的输出。在这个例子中,使用了一个浏览器会话、一个 FTP会话和ping来生成网络流量。@hist_log提取器是一个以 2为底数的对数柱状图(如下所示)。还可以步骤其他柱状图,从而使您能够定义 bucket的大小。
清单 11. nethist.stp的柱状图输出
$ sudo stap nethist.stp Capturing... ^C value |-------------------------------------------------- count 8 | 0 16 | 0 32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 1601 64 |@ 52 128 |@ 46 256 |@@@@ 164 512 |@@@ 140 1024 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2033 2048 | 0 4096 | 0
$ |