1 SystemTap简介
SystemTap是一个诊断Linux系统性能或功能问题的开源工具。它允许开发人员和系统管理员深入研究内核甚至用户空间应用程序的行为,以便发现错误状态、性能问题,或者仅仅为了解系统是如何工作的。它使得对运行时的Linux系统进行诊断调式变得更容易、更简单。通过使用SystemTap,可以很容易地总结和可视化与系统工作有关的信息,使您能够轻松地获得处理简单和复杂问题所需的信息
。
为了诊断系统问题或性能,开发者或调试人员只需要写一些脚本,而且SystemTap本身也提供了很多脚本,称为"tapset"方便开发。通过SystemTap提供的命令行接口就可以对正在运行的内核进行诊断调试,以前需要的修改或插入调试代码、重新编译内核、安装内核和重启动等这些琐碎的工作完全消除
当前该项目的主要开发人员为来自Red Hat, IBM, Intel和Hitachi的工程师。其中Redhat主要负责脚本转换/翻译器和运行时库,IBM负责kprobe和relayfs,Intel负责转换器安全检查以及performance monitor tapset。
2 SystemTap架构
2.1 Kprobes简介
SystemTap用于检查运行的内核的两种方法是 Kprobe和返回探针(return probes)。
Kprobes从 2.6.9 版本开始就添加到主流的 Linux 内核中,并且为探测内核提供一般性服务,最重要的两种服务是 Kprobe 和 Kretprobe。
Kprobe是一个通用钩子,可以插入内核代码中的几乎任何地方。为了允许它探测一条指令,该指令的第一个字节被替换为所使用架构的断点指令。当命中这个断点时,Kprobe接管并执行对应的探针处理程序代码,执行完成之后,接着执行原始的指令(从断点开始)。
Kretprobe(返回探针)有所不同,它操作调用函数的返回结果。因为一个函数可能有多个返回点,它实际使用一种称为 trampoline(蹦床) 的简单技术。它向函数条目添加一小段代码,而不是检查函数中的每个返回点。这段代码使用 trampoline 地址替换堆栈上的返回地址。当函数退出时,它返回到trampoline地址,而不是最初设置它返回的位置,并调用探测的处理程序代码。然后从 Kretprobe 返回到实际的调用方。Kretprobe的工作原理如图2-1所示。
2.2 SystemTap流程
SystemTap的基本流程主要涉及到 3 个交互实用程序stap、staprun和stapio,以及如下5个阶段:
1. 解析阶段:将stap脚本解析成语法解析树,会执行预处理,以及语义和语法正确性检查。
2. 细化阶段:解析脚本中的符合和引用,导入脚本中引用的脚本库tapsets(目录一般为/usr/share/SystemTap/tapset/)和内核调试信息。
3. 转化阶段:结合细化阶段的输出,将解析树转换成 C 源代码。
4. 构建阶段:将C代码编译成可以在内核中动态加载和执行的内核模块。
5. 执行阶段:staprun 和 stapio负责将模块安装到内核中并将输出发送到 stdout。如果在shell中按组合键 Ctrl-C或脚本退出,将执行清除进程,这将导致卸载模块并退出所有相关的实用程序。SystemTa在加载这个模块后,会开启脚本中的探测点。这个功能由systemtap-runtime包中的staprun提供;当某个事件发生时,对应的处理句柄(handler)就会执行;当SystemTap会话终止时,探测就会停止,对应的内核模块也会卸载。
SystemTap的流程如图2-2和图2-3所示。
SystemTap 的一个有趣特性是缓存脚本转换的能力。如果安装后的脚本没有更改,您可以使用现有的模块,而不是重新构建模块。图2-4显示了user-space和kernel-space 元素以及基于 stap的转换流程。
2.3 SystemTap原理
SystemTap的核心思想是定义一个事件(event),以及给出处理该事件的句柄(Handler)。当一个特定的事件发生时,内核运行该处理句柄,就像快速调用一个子函数一样,处理完之后恢复到内核原始状态。这里有两个概念:
-
事件(Event):SystemTap定义了很多种事件,例如进入或退出某个内核函数、定时器时间到、整个SystemTap会话启动或退出等等。
-
句柄(Handler):就是一些脚本语句,描述了当事件发生时要完成的工作,通常是从事件的上下文提取数据,将它们存入内部变量中,或者打印出来。
SystemTap工作原理是通过将脚本语句翻译成C语句,编译成内核模块。模块加载之后,将所有探测的事件以钩子的方式挂到内核上,当任何处理器上的某个事件发生时,相应钩子上句柄就会被执行。最后,当SystemTap会话结束之后,钩子从内核上取下,移除模块。整个过程用一个命令 stap 就可以完成。
SystemTap使用了类似于awk和C语言的脚本语言(类似于Dtrace的D语言)。它只使用了三种数据类型:整数(integers)、字符串(strings)和关联数组(associative Arrays)。它有完整的控制结构,包括块(blocks)、条件(conditionals)、循环(loops)和函数(functions)。语句分割符;是可选的,变量不需要声明类型,它们是根据上下文自动推测和检查的,它使用了kprobe提供的接口来实现探测,对于每一个探测,需要定义探测点以及相应的处理函数,探测点就是指kprobe中被探测的函数或指令地址(也被称为内核事件),但在SystemTap中,用户可以指定原文件,原代码的某一行,或者一个异步事件,如周期性的定时器,探测点使用了层次化的命名方式,探测点处理函数能够立刻输出数据,与printk很类似,它也能查看内核数据。脚本被一个翻译器转换成C代码并编译成一个内核模块。探测点根据内核的DWARF调试信息映射到内核的虚地址(因此SystemTap要求用户必须准备好可用的内核调试信息),所有的脚本内容在转换时进行严格的检查,并且在运行时也要检查(如无限循环、内存使用、递归和无效指针等),因此有好的安全性,不会影响正在运行的系统(这对生产系统是非常重要的)。 SystemTap包含了一个黑名单,其中列出的函数不能被SystemTap探测,因为它们会导致无限探测循环、锁重入等问题。
图2-5直观地给出了SystemTap的工作原理:
SystemTap脚本文件是.stp后缀的文件,使用的脚本语言是前面讲到的SystemTap自己定义的脚本语言,一个SystemTap脚本描述了将要探测的探测点以及定义了相关联的处理函数,每一个探测点对应于一个内核函数或事件或函数内部的某一位置。被关联的处理函数将在内核执行到对应的探测点时被执行。
tapsets是一个脚本库,包含了许多tapset,每一个tapset一般为某一内核子系统或特定的功能块预定义了一套探测点、辅助函数或全局变量供用户脚本或其它的tapset引用,它定义的一些数据能够被每一个探测点处理函数或脚本使用,这些数据通常通过使用处理函数语句块(HSB Handler Statement Block)来出口,HSB语句块中的变量就是被出口的数据。tapset一般由该内核子系统的开发者或对子系统非常了解的开发者编写,既使用了脚本语言,也使用了C语言,并且它已经被测试和验证,可以安全使用。tapsets属于SystemTap发行包的一部分。
SystemTap实现了一个脚本转换器/翻译器,当用户执行一个SystemTap脚本时,SystemTap将首先对它进行分析和一些安全检查,如果它引用了SystemTap预定义的脚本库提供的函数,SystemTap也将读取脚本库得到相应的代码,对于一些内核变量或符号的引用,它必须根据内核调试信息来解析到相应的地址。然后,它被转换成C代码,在这个转换中,SystemTap将根据需要增加必要的锁和安全检查代码。探测点之间共享的变量将被转换成恰当的静态声明并有锁保护,每组本地变量被转换到一个合成的调用帧结构中以避免消耗内核的栈空间。关联到探测点的处理函数被封装成一个接口函数,那调用恰当的kprobe接口函数来注册该探测点。
产生的C代码包含了一些对运行时tapset的引用,运行时tapset库提供了许多SystemTap接口函数,如通用的查询表、受限内存管理、启动、关闭、I/O操作以及其它一些函数。生成的C代码编译链接之后生成一个可加载的内核模块。为了快速得到运行结果,SystemTap使用了relayfs,当加载生成的内核模块后,该模块的初始化函数初始化自身,然后调用kprobe接口函数注册脚本中定义的探测点。当内核运行到注册的探测点时,相应的处理函数被调用,用户在处理函数中的输出语句将调用relayfs接口函数输出结果数据,用户在处理函数也可以调用一些内核的性能测量函数。当用户主动停止或脚本设定的条件满足时,模块将调用退出函数卸载已经注册的探测点并做一些清理处理就卸载模块自身。
SystemTap在运行时启动了一个进程,它专门负责通过relayfs读去模块的输出数据并即时地输出给用户。
3 SystemTap脚本编写
3.1 探针
SystemTap脚本由探针和在触发探针时需要执行的代码块组成。探针有许多预定义模式,表3-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 |
下面这个简单的探针例子,它在调用内核系统调用 sys_sync 时触发。当该探针触发时,您希望计算调用的次数,并发送这个计数以及表示调用进程名字和ID(PID)的信息。
首先,声明一个任何探针都可以使用的全局值(全局名称空间对所有探针都是通用的),然后将它初始化为0。其次,定义您的探针,它是一个探测内核函数 sys_sync的条目。与探针相关联的脚本将递增 count变量,然后发出一条消息,该消息定义调用的次数和当前调用的进程名字和PID。
global count=0
probe kernel.function("sys_sync") {
count++
printf("sys_sync called %d times, currently by execname:%s pid:%d\n",
count, execname(), pid());
}
运行结果如下:
3.2 元素
SystemTap 允许定义多种类型的变量,但类型是从上下文推断得出的,因此不需要使用类型声明。在 SystemTap 中,您可以找到数字(64 位签名的整数)、整数(64 位)、字符串和字面量(字符串或整数)。您还可以使用关联数组和统计数据。
SystemTap 提供 C 语言中常用的所有必要操作符,并且用法也是一样的。您还可以找到算术操作符、二进制操作符、赋值操作符和指针废弃。您还看到从 C 语言带来的简化,其中包括字符串连接、关联数组元素和合并操作符。
3.3 语句
在探针内部,SystemTap 提供一组类似于C一样易于使用的语句。注意,尽管该语句允许您开发复杂的脚本,但每个探针只能执行 1000 条语句(这个数量是可配置的)。表3-2列出了一小部分语句作为例子。注意,在这里的许多元素和 C 中的一样,尽管有一些附加的东西是特定于SystemTap的。
语句 | 说明 |
---|---|
if (exp) {} else {} | 标准的 if-then-else 语句 |
for (exp1 ; exp2 ; exp3 ) {} | 一个 for 循环 |
while (exp) {} | 标准的 while 循环 |
do {} while (exp) | 一个 do-while 循环 |
break | 退出迭代 |
continue | 继续迭代 |
next | 从探针返回 |
3.4 函数
SystemTap 提供许多内部函数,这些函数提供关于当前上下文的额外信息。例如,您可以使用 caller() 识别当前的调用函数,使用 cpu() 识别当前的处理器号码,以及使用 pid() 返回 PID,对调用堆栈访问等。表3-2列出了一小部分函数
函数名 | 说明 |
---|---|
execname() | 获取当前进程的名称,即可执行文件的名称 |
pid() | 获取当前进程的PID |
tid() | 获取当前线程的ID |
cpu() | 获取当前CPU的ID |
pp() | 描述当前被处理的探针点的字符串。例如probe process.syscall,process.end { /* scripts */},在块中调用pp()会返回"process.syscall"和"process.end"。 |
probefunc() | 获取当前probe的函数名称。例如probe sys_read函数,在probe块中调用该函数就会返回sys_read。注意这个函数的返回值是从pp()返回的字符串中解析得到的。 |
ppfunc() | 获取当前probe的函数名称。在probe指定文件中的函数中时非常有用,可以知道当前的probe位于哪个函数。 |
gettimeofday_s() | 自从Epoch开始的秒数,相同的还有ms、us和ns |
tz_ctime(sec) | 根据输入的秒数,转换为date格式的字符串。tz_ctime(gettimeofday_s()) |
get_cycles() | 获取处理器周期数 |
print_backtrace() | 打印内核调用栈信息 |
print_ubacktrace() | 打印用户态调用栈信息 |
thread_indent() | 输出当前线程的信息,格式为"相对时间 程序名称(线程id):(空格)",如果当probe的函数执行的次数约到,空格的数量也就越多。这个函数还有一个参数,用来控制空格的数量。如果参数值越大,则空格的数量越多。相对时间是当前的时间(以微秒为单位)减去指定线程第一次执行thread_indent时的时间。 |
下面给出调用这些函数一个脚本示例
probe begin,end {
sec = gettimeofday_s()
printf("pp() = %s, probefunc() = %s, ppfunc() = %s\n",
pp(), probefunc(), ppfunc())
printf("execname = %s, pid = %d, tid = %d, cpu = %d\n",
execname(), pid(), tid(), cpu())
printf("gettimeofday_s() = %ld.\n", sec)
printf("tz_ctime(sec) = %s.\n", tz_ctime(sec))
printf("get_cycles() = %d.\n", get_cycles())
}
probe kernel.function("sys_sync") {
printf("\n====sys_sync entry====\n")
printf("pp() = %s, probefunc() = %s, ppfunc() = %s\n",
pp(), probefunc(), ppfunc())
printf("execname = %s, pid = %d, tid = %d, cpu = %d\n",
execname(), pid(), tid(), cpu())
print_backtrace()
thread_indent(1)
}
probe kernel.function("sys_sync").return {
printf("\n====sys_sync return====\n")
printf("pp() = %s, probefunc() = %s, ppfunc() = %s\n",
pp(), probefunc(), ppfunc())
printf("execname = %s, pid = %d, tid = %d, cpu = %d\n",
execname(), pid(), tid(), cpu())
}
运行结果如下: