现在的位置: 首页 > 综合 > 正文

Systemtap学习笔记

2017年12月14日 ⁄ 综合 ⁄ 共 18899字 ⁄ 字号 评论关闭

SystemTap 的架构

让我们深入探索 SystemTap的某些细节,理解它如何在运行的内核中提供动态探针。您还将看到 SystemTap是如何工作的,从构建进程脚本到在运行的内核中激活脚本。

动态地检查内核

SystemTap 用于检查运行的内核的两种方法是Kprobes返回探针。但是理解任何内核的最关键要素是内核的映射,它提供符号信息(比如函数、变量以及它们的地址)。有了内核映射之后,就可以解决任何符号的地址,以及更改探针的行为。

Kprobes 2.6.9版本开始就添加到主流的 Linux内核中,并且为探测内核提供一般性服务。它提供一些不同的服务,但最重要的两种服务是
Kprobe
KretprobeKprobe特定于架构,它在需要检查的指令的第一个字节中插入一个断点指令。当调用该指令时,将执行针对探针的特定处理函数。执行完成之后,接着执行原始的指令(从断点开始)。

Kretprobes 有所不同,它操作调用函数的返回结果。注意,因为一个函数可能有多个返回点,所以听起来事情有些复杂。不过,它实际使用一种称为trampoline的简单技术。您将向函数条目添加一小段代码,而不是检查函数中的每个返回点。这段代码使用
trampoline
地址替换堆栈上的返回地址 —— Kretprobe地址。当该函数存在时,它没有返回到调用方,而是调用 Kretprobe(执行它的功能),然后从
Kretprobe
返回到实际的调用方。

SystemTap 的流程

1展示了 SystemTap的基本流程,涉及到
3
个交互实用程序和 5个阶段。该流程首先从 SystemTap脚本开始。您使用stap实用程序将
stap
脚本转换成提供探针行为的内核模块。stap流程从将脚本转换成解析树开始 (pass 1)。然后使用细化(elaboration)步骤
(pass 2)
中关于当前运行的内核的符号信息解析符号。接下来,转换流程将解析树转换成C源代码 (pass 3)并使用解析后的信息和tapset脚本SystemTap定义的库,包含有用的功能)。stap的最后步骤是构造使用本地内核模块构建进程的内核模块
(pass 4)

图 1. SystemTap 流程

有了可用的内核模块之后,stap完成了自己的任务,并将控制权交给其他两个实用程序 SystemTapstaprunstapio。这两个实用程序协调工作,负责将模块安装到内核中并将输出发送到
stdout (pass 5)
。如果在 shell中按组合键 Ctrl-C或脚本退出,将执行清除进程,这将导致卸载模块并退出所有相关的实用程序。

SystemTap 的一个有趣特性是缓存脚本转换的能力。如果安装后的脚本没有更改,您可以使用现有的模块,而不是重新构建模块。图 2显示了
user-space
kernel-space元素以及基于 stap的转换流程。

图 2. 从 kernel/user-space 角度了解 SystemTap 流程


回页首

SystemTap 脚本编写

SystemTap中编写脚本非常简单,但也很灵活,有许多您需要使用的选项。参考资料提供一个详述语言和可行性的手册的链接,但这个小节仅讨论一些例子,让您初步了解
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")

调用 ext3write函数时触发

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
 
$

 

标识符

标识符用于表示变量名和函数名,一般由字母数字序列构成,也包括下划线(_)和美元符号($)除了$也是合法的语法之外,它们与C语言标识符的语法一样。标识符可以以$开始,以美元符号$开头的标识符被翻译成目标代码中的变量,而不是systemtap脚本中的变量。当然标识符不能用一个普通的数字开始。

eg:

ptr是在探测上下文 的一个内核指针

$ptr->member

数据类型

SystemTap语言包括少量的数据类型(整型和字符串),但没有类型声明。变量类型是根据它的使用来推断的。为了支持这个推断功能翻译器强制执行类型一致性,类型包括函数参数类型和返回值类型,数组索引类型和数组值类型。字符串和数字之间有没有隐式类型转换。类型使用的不一致会导致一个标识符错误信号。

注释

Systemtap脚本支持三种方式的注释:

# ...  shell style, to the end of line

// ...  C++ style, to the end of line

/* ...  C style ... */

 表达式

SystemTap 支持许多运算符,这些运算符和C语言和awk语言一样,使用相同的语法、语义、优先级。

if/else statemen

while(EXPR)STATEMENT

 for(A;B;C)STATEMENT

指针类型转换

使用@cast()操作符支持类型转换。system tap脚本为长整型(个人理解为具有众多成员的结构体)定义一个指针,然后就可以存取类型成员,使用语法类似$符号,存取目标变量。在一个指针被保存进一个长整型变量指针脚本之后,翻译器失去必要的类型信息去存取指针成员。The @cast() 操作符告诉翻译器如何读取一个指针。

下面的语句将P翻译成一个指向名为type_name的结构体或联合体的指针,并对指针解引用获取其成员的值,可选模块参数告诉翻译器在哪个寻找该类型的信息:

@cast(p,"type_name"[, "module"])->member

@cast(pointer, "task_struct","kernel")->parent->tgid

 

翻译器也可以创建它自己的类型信息模块,可以借助头文件

@cast(tv,"timeval", "<sys/time.h>")->tv_sec

@cast(task,"task_struct", "kernel<linux/sched.h>")->tgid

内嵌c

终端用户脚本也许不含有内嵌的c语句,除非systemtap使用了-g("guru"模式)的选项。

嵌入式c语言可以是一个脚本函数的函数体。此时函数体不能使用{},而是使用%{ %}。任何嵌入的c语言都会被逐句翻译到内核模块中去,它的安全性和正确性程序员自己保证。为了获得参数,并得到返回值,可以使用STAP_ARG_* 和 STAP_RETVALUE.我们知道的数据收集的函数pid(),execname(),以及类似的函数都是内嵌的c函数。


这里有一些安全相关的约束,当使用内嵌c时,应该遵守。

1.不要对未知的指针进行解引用。

2.不要调用会引起睡眠或者错误的内核例程。

3.当心可能的不良的递归,你的内嵌c函数调用的例程,这个例程可能是探针的对象。如果那样的探针处理函数调用了你的内嵌的c函数,你可能要遭遇无限的递归。类似的问题也发生在不可重用的锁上。

4。如果必须要对一个数据结构上锁,就是用trylock。即是上锁不成功,也不会阻塞

应用:

对管理员,SystemTap可用于监控系统性能,找出系统瓶颈,而对于开发者,可以查看他们的程序运行时在linux系统内核内部的运行情况。主要用于查看内核空间事件信息,对用户空间事件的探测,目前正加紧改进。

安装

1、SystemTap的安装及使用需要针对正在使用的内核安装相应的kernel-devel、kernel-debuginfo和kernel-debuginfo-common包,以插入探针。

2、安装SystemTap和SystemTap-runtime包

3、使用如下命令测试一下:

  

stap -v -e 'probe vfs.read {printf("read performed/n"); exit()}'

 

为目标机产生SystemTap instrumentation:

这样就可以在一台机器上为多种内核产生SystemTap instrumentation,而且目标机上只安装SystemTap-runtime即可。

操作如下:

1.        在目标机上安装systemtap-runtime RPM包;

2.        使用uname –r查看目标机内核;

3.        在host system上安装SystemTap;

4.        在host system上安装目标机内核及相关RPMs

5.        在host name上运行命令:

stap -r kernel_version script -m module_name

 

6.        把新产生的模块拷贝到目标机,并运行如下命令:

staprun module_name.ko

 

注意:host system和目标机架构及操作系统版本必须一致。

 

运行SystemTap脚本

运行stap和staprun需要被授以权限,一般用户需要运行SystemTap,则需要被加入到以下用户组的一个:1、stapdev:用stap编译SystemTap脚本成内核模块,并加载进内核;2、stapusr:仅能运行staprun加载/lib/modules/kernel_version/systemtap/目录下模块。

SystemTap Flight Recorder模式

该模式允许长时间运行SystemTap脚本,但仅focus on 最近的输出,有2个变种:in-memory和file模式,两种情况下SystemTap都作为后台进程运行。

In-memory模式:

stap -F iotime.stp

一旦脚本启动后,你可以看到以下输出信息以辅助命令重新连到运行中的脚本:

Disconnecting from systemtap module.

To reconnect, type "staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556"

当感兴趣的事件发生时,可以重新连接到运行中的脚本,并在内存Buffer中输出最近的数据并持续输出:

staprun -A stap_5dd0073edcb1f13f7565d8c343063e68_19556

内存Buffer默认1MB,可以使用-S选项,例如-S2指定为2MB

File Flight Recorder

stap -F -o /tmp/pfaults.log -S 1,2  pfaults.stp

命令结果输出到/tmp/pfaults.log.[0-9],每个文件1MB,并且仅保存最近的两个文件,-S指定了第一个参数:每个输出文件大小1MB,第二个参数:仅保留最近的两个文件,systemtap在pfaults.log后面加.[0-9]后缀。

该命令的输出是systemtap脚本进程ID,使用如下命令可以终止systemtap脚本

kill -s SIGTERM 7590

运行ls –sh /tmp/pfaults.log.*

1020K /tmp/pfaults.log.5    44K /tmp/pfaults.log.6

SystemTap如何工作

SystemTap的基本工作原理就是:event/handler,运行systemtap脚本产生的加载模块时刻监控事件的发生,一旦发生,内核就调用相关的handler处理。

 

一运行一个SystemTap脚本就会产生一个SystemTap session:

1.        SystemTap检查脚本以及所使用的相关tapset库;

2.        SystemTap将脚本转换成C语言文件,并运行C语言编译器编译之创建一个内核模块;

3.        SystemTap加载该模块,从而使用所有探针(events和handlers);

4.        事件发生时,执行相关handlers

5.        一旦SystemTap session停止,则探针被禁止,该内核模块被卸载。

 

探针:event及其handler,一个SystemTap脚本可以包含多个探针。

SystemTap脚本以.stp为扩展名,其基本格式如下所示:

probe event {statements}

允许一个探针内多个event,以,隔开,任一个event发生时,都会执行statements,各个语句之间不需要特殊的结束符号标记。而且可以在一个statements block中包含其他的statements block。

函数编写:

function function_name(arguments) {statements}

probe event {function_name(arguments)}

SystemTap Event

可大致划分为synchronous和asynchronous。

同步事件:

执行到定位到内核代码中的特定位置时触发event

1.        syscall.system_call

系统调用入口和exit处:syscall.system_call和syscall.system_call.return,比如对于close系统调用:syscall.close和syscall.close.return

2.        vfs.file_operation

vfs.file_operation和vfs.file_operation.return

3.        kernel.function("function")

如:kernel.function(“sys_open”)和kernel.function(“sys_open”).return

可使用*来代表wildcards:

probe kernel.function("*@net/socket.c") { }

probe kernel.function("*@net/socket.c").return { }

代表了net/socket.c中所有函数的入口和exit口。

4.        kernel.trace("tracepoint")

2.6.30及newer为内核中的特定事件定义了instrumentation,入kernel.trace(“kfree_skb”)代表内核中每次网络buffer被释放掉时的event。

5.        module("module").function("function")

probe module("ext3").function("*") { }

probe module("ext3").function("*").return { }

系统内核模块多存放在/lib/modules/kernel_version

AsynchronousEvents

不绑定到内核的特定指令或位置处。包括:

1、  begin:SystemTap session开始时触发,当SystemTap脚本开始运行时触发;

2、  end :SystemTap session终止时触发;

3、  timer事件:

probe timer.s(4)

{

  printf("hello world/n")

}

•timer.ms(milliseconds)

•timer.us(microseconds)

•timer.ns(nanoseconds)

• timer.hz(hertz)

• timer.jiffies(jiffies)

可查看man stapprobes来查看其它支持的events

SystemTap Handler/Body

支持的函数:

1、  printf ("format string/n", arguments),%s:字符串,%d数字,以 , 隔开;

2、  tid():当前线程ID;

3、  uid():当前用户ID;

4、  cpu():当前CPU号;

5、  gettimeofday_s():自从Epoch开始的秒数;

6、  ctime()将从Unix Epoch开始的秒数转换成date;

7、  pp():描述当前被处理的探针点的字符串;

8、  thread_indent():

probe kernel.function("*@net/socket.c")

{

  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

函数thread_indent()只有1个参数:代表对线程的”indentation counter”的增减数,即系统调用显示的步数,返回字符串(自从第一次调用thread_indent()以来的描述:进程名(进程ID))

9、  name

标记系统调用的名字,仅用于syscall.system_call中。

 

10、             target()

stap script -x process IDor stap script -c command联合使用,如果想在脚本中获得进程ID或命令可以如此做

probe syscall.* {

  if (pid() == target())

    printf("%s/n", name)

}

 

SystemTap Handler构造

变量

1、  不必事先声明,直接使用即可,由SystemTap自动判断其属于string还是integer,整数则默认为0,默认在probe中声明的是local变量

2、  在各个probe之间共享的变量使用global声明

global count_jiffies, count_ms

probe timer.jiffies(100) { count_jiffies ++ }

probe timer.ms(100) { count_ms ++ }

probe timer.ms(12345)

{

  hz=(1000*count_jiffies) / count_ms

  printf ("jiffies:ms ratio %d:%d => CONFIG_HZ=%d/n",

    count_jiffies, count_ms, hz)

      exit ()

}

Target变量

Probe event可以映射到代码的实际位置,如kernel.function(“function”)、kernel.statement(“statement”),这允许使用target变量来记录代码中指定位置处可视变量的值。

运行如下命令:可以显示指定vfs_read处可视target变量

stap -L 'kernel.function("vfs_read")'

显示

kernel.function("vfs_read@fs/read_write.c:277") $file:struct file* $buf:char* $count:size_t

$pos:loff_t*

每个target变量以$开头:变量类型。如果是结构体类型,则SystemTap可以使用->来查看其成员。对基本类型,integer或string,SystemTap有函数可以直接读取address处的值,如:

kernel_char(address)

Obtain the character at address from kernel memory.

kernel_short(address)

Obtain the short at address from kernel memory.

kernel_int(address)

Obtain the int at address from kernel memory.

kernel_long(address)

Obtain the long at address from kernel memory

kernel_string(address)

Obtain the string at address from kernel memory.

kernel_string_n(address, n)

Obtain the string at address from the kernel memory and limits the string to n bytes.

 

打印target变量

$$vars:类似sprintf("parm1=%x ...parmN=%x var1=%x ... varN=%x", parm1, ..., parmN, var1, ..., varN),目的是打印probe点处的每个变量;

$$locals:$$vars子集,仅打印local变量;

$$parms:$$vars子集,仅包含函数参数;

$$return:仅在return probes存在,类似sprintf("return=%x",$return),如果没有返回值,则是空串

例子如下:

stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms); exit(); }'

函数vfs_read有4个参数:file、buf、count和pos,输出如下:

file=0xffff8800b40d4c80 buf=0x7fff634403e0 count=0x2004 pos=0xffff8800af96df48

如果你想知道数据结构里面的成员信息,可以在”$$params”后面加一个”$”,如下所示:

stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms$); exit(); }'

输出如下:

file={.f_u={...}, .f_path={...}, .f_op=0xffffffffa06e1d80, .f_lock={...}, .f_count={...}, .f_flags=34818, buf="" count=8196 pos=-131938753921208

仅一个”$”表示,不展开数据结构域成员,如想展开,则需使用”$$”

stap -e 'probe kernel.function("vfs_read") {printf("%s/n", $$parms$$); exit(); }'

输出受限于最大字符串大小:

file={.f_u={.fu_list={.next=0xffff8801336ca0e8, .prev=0xffff88012ded0840}, .fu_rcuhead={.next=0xffff8801336ca0e8

强制类型转换

大多数情况下,SystemTap都可以从debuginfo中获得变量类型,但对于代码中void指针则debuginfo中类型信息不可用,同样probe handler里面的类型信息在function里面也不可用,怎么办呢?

 

SystemTap函数参数使用long来代替typed pointer,SystemTap的@cast操作可以指出对象正确类型:

function task_state:long (task:long)

{

return @cast(task, "task_struct", "kernel<linux/sched.h>")->state

}

第一个参数是指向对象的指针,第二个参数是将该对象(参数1)要强制类型转换成的类型,第三个参数指出类型定义的出处,是可选的。

检查Target变量可用性

随着代码运行,变量可能失效,因此需要用@defined来判断该变量是否可用:

probe vm.pagefault = kernel.function("__handle_mm_fault@mm/memory.c") ?,

kernel.function("handle_mm_fault@mm/memory.c") ?

{

name = "pagefault"

write_access = (@defined($flags)

? $flags & FAULT_FLAG_WRITE : $write_access)

address = $address

}

条件语句

if (condition)

statement1

else

statement2

 

global countread, countnonread

probe kernel.function("vfs_read"),kernel.function("vfs_write")

{

if (probefunc()=="vfs_read")

countread ++

else

countnonread ++

}

probe timer.s(5) { exit() }

probe end

{

printf("VFS reads total %d/n VFS writes total %d/n", countread, countnonread)

}

 

循环语句

while (condition)

statement

 

for (initialization; conditional; increment) statement

比较:

==、>=、<=、!=

命令行参数:

使用$标志着希望输入的是integer类型命令行参数,@:string

probe kernel.function(@1) { }

probe kernel.function(@1).return { }

关联数组

关联数组一般在multiple probes里面处理,所以必须声明为global,不管是在一个还是多个probes里面用,要读取数组成员值,可以:

array_name[index_expression]

如下所示:

foo["tom"] = 23

foo["dick"] = 24

foo["harry"] = 25

一个索引可以包含最多9个索引表达式,用 , 隔开:

device[pid(),execname(),uid(),ppid(),"W"] = devname

 

SystemTap的数组操作

赋值:

array_name[index_expression] = value

例子:索引和值可以使用handler function:

foo[tid()] = gettimeofday_s()

每次触发这个语句,多次后就会构成一个关联数组,如果tid()返回值在foo索引中已有一个,则用新值代替旧值。

读取数组值:

delta = gettimeofday_s() - foo[tid()]

如果无法找到指定”索引”对应的值,则数组读返回0(int)或null/empty值(string)

增加关联数组值

array_name[index_expression] ++

处理数组的多个成员:

global reads

probe vfs.read

{

reads[execname()] ++

}

probe timer.s(3)

{

foreach (count in reads)

printf("%s : %d /n", count, reads[count])

}

这个foreach无序打印所有reads数组值,如果想升序/降序,则需要使用升序(+)、降序(-),也可以限制处理的数组数目:

probe timer.s(3)

{

foreach (count in reads- limit 10)

printf("%s : %d /n", count, reads[count])

}

Clearing/Deleting数组和数组成员

global reads

probe vfs.read

{

reads[execname()] ++

}

probe timer.s(3)

{

foreach (count in reads)

printf("%s : %d /n", count, reads[count])

delete reads

}

使用delete操作来删除数组成员或整个数组。

global reads, totalreads

probe vfs.read

{

reads[execname()] ++

totalreads[execname()] ++

}

probe timer.s(3)

{

printf("=======/n")

foreach (count in reads-)

printf("%s : %d /n", count, reads[count])

delete reads

}

probe end

{

printf("TOTALS/n")

foreach (total in totalreads-)

printf("%s : %d /n", total, totalreads[total])

}

在if语句中使用数组:

global reads

probe vfs.read

{

reads[execname()] ++

}

probe timer.s(3)

{

printf("=======/n")

foreach (count in reads-)

if (reads[count] >= 1024)

printf("%s : %dkB /n", count, reads[count]/1024)

else

printf("%s : %dB /n", count, reads[count])

}

检查成员

可以检查是否一个指定健是数组键值:

if([index_expression] in array_name) statement

 

global reads

probe vfs.read

{

reads[execname()] ++

}

probe timer.s(3)

{

printf("=======/n")

foreach (count in reads+)

printf("%s : %d /n", count, reads[count])

if(["stapio"] in reads) {

printf("stapio read detected, exiting/n")

exit()

}

}

计算统计集合

统计集合用于收集数值的统计信息,用于计算新值

global reads

probe vfs.read

{

reads[execname()] <<< count

}

操作符<<<用于将count返回的值存放在read数组中execname()相关的值中,即一个键值关联多个相关值。

为计算统计信息,使用@extractor(variable/arrayindex expression),extractor可以是如下integer extractor:

count:@count(writes[execname()])返回存放在writes数组中某单一键值对应的值数目;

sum:@sum(writes[execname()])返回在writes数组中某单一键值对应的值的和

min:最小值

max:最大值

avg:variable/array作为索引的统计集合中数据的平均值

global reads

probe vfs.read

{

reads[execname(),pid()] <<< 1

}

probe timer.s(3)

{

foreach([var1,var2] in reads)

printf("%s (%d) : %d /n", var1, var2, @count(reads[var1,var2]))

}

Tapsets

Tapsets是脚本库,里面预写好了probes和functions可以被SystemTap脚本调用,tapsets也使用.stp作为后缀,默认位于:/usr/share/systemtap/tapset,但无法直接运行。

抱歉!评论已关闭.