SystemTap 提供了一个命令行界面和一个脚本语言,用于详细检查正在运行的 Linux 系统的活动,特别是内核。SystemTap 脚本使用 SystemTap 脚本语言编写,然后编译为 C 代码内核模块并插入内核。这些脚本可以设计为提取、过滤和汇总数据,从而可以诊断复杂的性能问题或功能问题。SystemTap 提供的信息类似于 netstat、ps、top 和 iostat 等工具的输出。但是,可以对收集的信息使用更多的过滤和分析选项。
每次运行 SystemTap 脚本时,都会启动一个 SystemTap 会话。在允许脚本运行之前,会对脚本进行多次处理。然后,脚本编译为内核模块并加载。如果脚本之前已执行过,并且没有系统组件发生更改(例如,不同的编译器或内核版本、库路径或脚本内容),SystemTap 不会重新编译脚本。相反,它使用存储在 SystemTap 缓存 (~/.systemtap) 中的 *.c 和 *.ko 数据。
当 tap 完成运行时,模块将被卸载。有关示例,请参阅 第 4.2 节,“安装和设置” 中的测试运行及其相应的说明。
SystemTap 的使用基于 SystemTap 脚本 (*.stp)。它们告诉 SystemTap 收集哪种类型的信息,以及在收集到该信息后该做什么。这些脚本使用类似于 AWK 和 C 的 SystemTap 脚本语言编写。有关语言定义,请参阅 https://sourceware.org/systemtap/langref/。许多有用的示例脚本可从 https://www.sourceware.org/systemtap/examples/ 获取。
SystemTap 脚本的基本思想是命名 事件,并为它们提供 处理程序。当 SystemTap 运行脚本时,它会监视某些事件。当事件发生时,Linux 内核将处理程序作为子例程运行,然后恢复。因此,事件充当处理程序运行的触发器。处理程序可以记录指定的数据并以某种方式打印它。
SystemTap 语言仅使用少量数据类型(整数、字符串和这些的关联数组),以及完整的控制结构(块、条件、循环、函数)。它具有轻量级的标点符号(分号是可选的),并且不需要详细的声明(类型是自动推断和检查的)。
有关 SystemTap 脚本及其语法的更多信息,请参阅 第 4.3 节,“脚本语法” 以及 stapprobes 和 stapfuncs man 页面,这些页面与 systemtap-docs 包一起提供。
Tapsets 是预先编写的探针和函数的库,可以在 SystemTap 脚本中使用。当用户运行 SystemTap 脚本时,SystemTap 会将脚本的探针事件和处理程序与 tapset 库进行检查。然后,SystemTap 在将脚本翻译为 C 之前加载相应的探针和函数。与 SystemTap 脚本本身一样,tapsets 也使用文件扩展名 *.stp。
但是,与 SystemTap 脚本不同,tapsets 不用于直接执行。它们构成其他脚本可以从中提取定义的库。因此,tapset 库是一个抽象层,旨在使用户更容易定义事件和函数。Tapsets 为用户可能希望指定为事件的函数提供别名。了解正确的别名通常比记住可能因内核版本而异的特定内核函数更容易。
与 SystemTap 关联的主要命令是 stap 和 staprun。要执行它们,您需要 root 权限,或者必须是 stapdev 或 stapusr 组的成员。
stap
SystemTap 前端。运行 SystemTap 脚本(来自文件或标准输入)。它将脚本翻译为 C 代码,对其进行编译,并将生成的内核模块加载到正在运行的 Linux 内核中。然后,执行请求的系统跟踪或探针函数。
staprun
SystemTap 后端。加载和卸载 SystemTap 前端生成的内核模块。
要获取每个命令的选项列表,请使用 --help。有关详细信息,请参阅 stap 和 staprun man 页面。
为了避免仅为了启用 SystemTap 而向用户授予 root 访问权限,请使用以下 SystemTap 组。它们默认情况下在 openSUSE Leap 上不可用,但您可以创建这些组并相应地修改访问权限。此外,如果安全影响适合您的环境,请调整 staprun 命令的权限。
stapdev
此组的成员可以使用 stap 运行 SystemTap 脚本,或使用 staprun 运行 SystemTap 仪器模块。由于运行 stap 涉及将脚本编译为内核模块并将其加载到内核中,因此此组的成员仍然具有有效的 root 访问权限。
stapusr
此组的成员仅允许使用 staprun 运行 SystemTap 仪器模块。此外,他们只能从 /lib/modules/KERNEL_VERSION/systemtap/ 运行这些模块。此目录必须由 root 拥有,并且只能由 root 用户写入。
以下列表概述了 SystemTap 的主要文件和目录。
/lib/modules/KERNEL_VERSION/systemtap/
保存 SystemTap 仪器模块。
/usr/share/systemtap/tapset/
保存标准 tapset 库。
/usr/share/doc/packages/systemtap/examples
保存用于不同目的的几个示例 SystemTap 脚本。仅当安装了 systemtap-docs 包时才可用。
~/.systemtap/cache
SystemTap 缓存文件的目录。
/tmp/stap*
SystemTap 文件的临时目录,包括翻译后的 C 代码和内核对象。
由于 SystemTap 需要有关内核的信息,因此必须安装一些额外的与内核相关的软件包。对于您想要使用 SystemTap 探测的每个内核,您需要安装以下软件包的集合。此集合应与内核版本和风味完全匹配(如下面的概述中指示的 *)。
如果你已订阅系统的在线更新,你可以在 openSUSE Leap 15.6 相关的 *-Debuginfo-Updates 在线安装存储库中找到“debuginfo”包。使用 YaST 启用存储库。
对于经典的 SystemTap 设置,请安装以下软件包(使用 YaST 或 zypper)。
systemtap
systemtap-server
systemtap-docs(可选)
kernel-*-base
kernel-*-debuginfo
kernel-*-devel
kernel-source-*
gcc
要访问 man 页面和有用的各种用于不同目的的 SystemTap 脚本示例集合,请另外安装 systemtap-docs 包。
要检查机器上是否正确安装了所有软件包以及 SystemTap 是否已准备好使用,请以 root 身份执行以下命令。
# stap -v -e 'probe vfs.read {printf("read performed\n"); exit()}'它通过运行脚本并返回输出来探测当前使用的内核。如果输出类似于以下内容,则 SystemTap 已成功部署并已准备好使用
Pass 1: parsed user script and 59 library script(s) in 80usr/0sys/214real ms. Pass 2: analyzed script: 1 probe(s), 11 function(s), 2 embed(s), 1 global(s) in 140usr/20sys/412real ms. Pass 3: translated to C into "/tmp/stapDwEk76/stap_1856e21ea1c246da85ad8c66b4338349_4970.c" in 160usr/0sys/408real ms. Pass 4: compiled C into "stap_1856e21ea1c246da85ad8c66b4338349_4970.ko" in 2030usr/360sys/10182real ms. Pass 5: starting run. read performed Pass 5: run completed in 10usr/20sys/257real ms.
检查脚本与 | |
检查脚本的组件。 | |
将脚本翻译为 C。运行系统 C 编译器从脚本创建内核模块。生成的 C 代码 ( | |
加载模块并启用脚本中的所有探针(事件和处理程序)通过钩入内核。正在探测的事件是虚拟文件系统 (VFS) 读取。由于事件发生在任何处理器上,因此会执行有效的处理程序(打印文本 | |
在 SystemTap 会话终止后,探针将被禁用,内核模块将被卸载。 |
如果测试期间出现任何错误消息,请检查输出中有关任何缺失软件包的提示,并确保正确安装了这些软件包。可能还需要重新启动并加载适当的内核。
SystemTap 脚本由以下两个组件组成
命名与关联处理程序应执行的内核事件。事件示例是在某个函数中进入或退出、定时器到期或启动或终止会话。
一系列脚本语言语句,指定在发生某个事件时要执行的工作。这通常包括从事件上下文中提取数据、将其存储到内部变量中或打印结果。
事件及其对应的处理程序统称为 探测。SystemTap 事件也称为 探测点。探测的处理程序也称为 探测体。
可以在 SystemTap 脚本中的不同样式中插入注释:使用 #、/* */ 或 // 作为标记。
SystemTap 脚本可以有多个探测。它们必须以以下格式编写
probe EVENT {STATEMENTS}每个探测都有一个相应的语句块。此语句块必须包含在 { } 中,并包含要为每个事件执行的语句。
以下示例显示了一个简单的 SystemTap 脚本。
probe1 begin2 {3 printf4 ("hello world\n")5 exit ()6 }7
探测的开始。 | |
事件 | |
处理程序定义的开始,由 | |
处理程序中定义的第一个函数: | |
要由 | |
处理程序中定义的第二个函数: | |
处理程序定义的结束,由 |
事件 begin 2(SystemTap 会话的开始)触发包含在 { } 中的处理程序。在这里,是 printf 函数 4。在这种情况下,它打印 hello world,后跟换行符 5。然后,脚本退出。
如果您的语句块包含多个语句,SystemTap 会按顺序执行这些语句——您不需要在多个语句之间插入特殊的分隔符或终止符。SystemTap 脚本中的语句块也可以嵌套在另一个语句块中。通常,SystemTap 脚本中的语句块使用与 C 编程语言相同的语法和语义。
SystemTap 支持几个内置事件。
事件的通用语法是点符号序列。这允许将事件命名空间分解为各个部分。每个组件标识符都可以通过字符串或数字字面量参数化,语法类似于函数调用。一个组件可以包含一个 * 字符,以扩展到其他匹配的探测点。一个探测点可以后跟一个 ? 字符,以表明它是可选的,并且如果扩展失败,不应产生错误。或者,探测点可以后跟一个 ! 字符,以表明它是可选的并且足够了。
SystemTap 支持每个探测点的多个事件——它们需要用逗号 (,) 分隔。如果单个探测点指定了多个事件,SystemTap 将在指定事件中的任何一个发生时执行处理程序。
事件可以分为以下几类
同步事件:当任何进程在内核代码的特定位置执行指令时发生。这为其他事件提供了一个参考点(指令地址),可以从中获得更多上下文数据。
同步事件的一个例子是 vfs.FILE_OPERATION:进入虚拟文件系统 (VFS) 的 FILE_OPERATION 事件。例如,在 第 4.2 节,“安装和设置” 中,read 是 VFS 中使用的 FILE_OPERATION 事件。
异步事件:不绑定到代码中的特定指令或位置。这一系列探测点主要由计数器、计时器和类似结构组成。
异步事件的例子有:begin(SystemTap 会话的开始——当运行 SystemTap 脚本时,end(SystemTap 会话的结束),或计时器事件。计时器事件指定一个处理程序,以便定期执行,例如 example timer.s(SECONDS),或 timer.ms(MILLISECONDS)。
与收集信息的其他探测点结合使用时,计时器事件允许您打印定期更新,并查看这些信息随时间的变化情况。
例如,以下探测点将每 4 秒打印一次文本 “hello world”
probe timer.s(4)
{
printf("hello world\n")
}有关受支持事件的详细信息,请参阅 stapprobes man 手册页。该 man 手册页的 参见 部分还包含指向讨论特定子系统和组件支持事件的其他 man 手册页的链接。
每个 SystemTap 事件都伴随着为该事件定义的相应处理程序,该处理程序由语句块组成。
如果您需要在多个探测点中使用相同的语句集,可以将它们放在一个函数中以便于重用。函数由关键字 function 后跟一个名称来定义。它们接受任意数量的字符串或数字参数(按值),并且可以返回一个字符串或数字。
function FUNCTION_NAME(ARGUMENTS) {STATEMENTS}
probe EVENT {FUNCTION_NAME(ARGUMENTS)}当 EVENT 的探测点执行时,将执行 FUNCTION_NAME 中的语句。ARGUMENTS 是可选值,传递到函数中。
函数可以在脚本中的任何位置定义。它们可以接受任何
在 示例 4.1,“简单的 SystemTap 脚本” 中已经介绍的常用函数之一是 printf 函数,用于以格式化的方式打印数据。在使用 printf 函数时,您可以使用格式字符串来指定应如何打印参数。格式字符串包含在引号中,可以包含进一步的格式说明符,以 % 字符开头。
使用哪些格式字符串取决于您的参数列表。格式字符串可以有多个格式说明符——每个说明符与相应的参数匹配。多个参数可以用逗号分隔。
printf 函数与格式说明符#上面的示例打印当前可执行文件的名称 (execname()) 作为字符串,并将进程 ID (pid()) 作为整数打印在方括号中。然后,一个空格、单词 open 和一个换行符跟随
[...] vmware-guestd(2206) open held(2360) open [...]
除了在 示例 4.3,“printf 函数与格式说明符” 中使用的两个函数 execname() 和 pid()) 之外,还可以将各种其他函数用作 printf 参数。
最常用的 SystemTap 函数包括以下内容
当前线程的 ID。
当前线程的进程 ID。
当前用户的 ID。
当前 CPU 编号。
当前进程的名称。
自 Unix 纪元(1970 年 1 月 1 日)以来的秒数。
将时间转换为字符串。
描述当前正在处理的探测点的字符串。
用于组织打印结果的有用函数。它(在内部)为每个线程 (tid()) 存储一个缩进计数器。该函数接受一个参数,一个缩进增量,指示要添加到线程缩进计数器的空格数或要删除的空格数。它返回一个字符串,其中包含一些通用的跟踪数据以及适当数量的缩进空格。返回的通用数据包括时间戳(自线程的初始缩进以来的微秒数)、进程名称和线程 ID 本身。这允许您识别哪些函数被调用、谁调用了它们以及它们花费了多长时间。
调用条目和退出通常不会立即彼此相邻(否则很容易将它们匹配)。在第一次调用条目及其退出之间,通常会进行其他调用条目和退出。缩进计数器帮助您将条目与其对应的退出匹配,因为它在下一个函数调用不是前一个函数的退出时,会缩进下一个函数调用。
有关受支持的 SystemTap 函数的更多信息,请参阅 stapfuncs man 手册页。
除了在 SystemTap 处理程序中可以使用的其他常用结构之外,还包括变量、条件语句(如 if/else、while 循环、for 循环、数组或命令行参数。
变量可以在脚本中的任何位置定义。要定义一个变量,只需选择一个名称并从函数或表达式分配一个值给它
foo = gettimeofday( )
然后您可以在表达式中使用该变量。根据分配给变量的值的类型,SystemTap 会自动推断每个标识符的类型(字符串或数字)。任何不一致之处都将报告为错误。在上面的示例中,foo 将自动被分类为数字,并且可以通过整数格式说明符 (%d) 通过 printf() 打印。
但是,默认情况下,变量是包含它们的探测点的局部变量。它们在每次处理程序调用时初始化、使用和释放。要共享探测点之间的变量,请在探测点外部使用 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 ()
}此示例脚本通过使用计数 jiffies 和毫秒的计时器,然后相应地计算,来计算内核的 CONFIG_HZ 设置。(jiffy 是系统计时器中断的一个持续时间。它不是绝对的时间间隔单位,因为其持续时间取决于特定硬件平台的时钟中断频率)。使用 global 语句,可以在探测点 timer.ms(12345) 中使用变量 count_jiffies 和 count_ms。使用 ++,变量的值将增加 1。
您可以在 SystemTap 脚本中使用几种条件语句。最常见的条件语句如下
它们表示如下
if (CONDITION)1STATEMENT12 else3STATEMENT24
if 语句将一个整数值表达式与零进行比较。如果条件表达式 1 非零,则执行第一个语句 2。如果条件表达式为零,则执行第二个语句 4。else 子句(3 和 4)是可选的。2 和 4 也可以是语句块。
它们表示如下
while (CONDITION)1STATEMENT2
当 condition 非零时,执行语句 2。2 也可以是语句块。它必须更改一个值,以便 condition 最终为零。
它们是 while 循环的快捷方式,表示如下
for (INITIALIZATION1; CONDITIONAL2; INCREMENT3) statement
在 1 中指定的表达式用于初始化循环计数器,并在循环开始执行之前执行。循环的执行将继续,直到循环条件 2 为假。(此表达式在每个循环迭代开始时检查)。在 3 中指定的表达式用于递增循环计数器。它在每个循环迭代结束时执行。
以下运算符可用于条件语句
==: 等于
!=: 不等于
>=: 大于或等于
<=: 小于或等于
如果您已安装 systemtap-docs 包,您可以在 /usr/share/doc/packages/systemtap/examples 中找到几个有用的 SystemTap 示例脚本。
本节更详细地描述了一个相当简单的示例脚本:/usr/share/doc/packages/systemtap/examples/network/tcp_connections.stp。
tcp_connections.stp 监控传入的 TCP 连接##! /usr/bin/env stap
probe begin {
printf("%6s %16s %6s %6s %16s\n",
"UID", "CMD", "PID", "PORT", "IP_SOURCE")
}
probe kernel.function("tcp_accept").return?,
kernel.function("inet_csk_accept").return? {
sock = $return
if (sock != 0)
printf("%6d %16s %6d %6d %16s\n", uid(), execname(), pid(),
inet_get_local_port(sock), inet_get_ip_source(sock))
}此 SystemTap 脚本监控传入的 TCP 连接,并帮助识别计算机上未经授权或不需要的网络访问请求。它显示以下信息,用于计算机接受的每个新的传入 TCP 连接
用户 ID (UID)
接受连接的命令 (CMD)
命令的进程 ID (PID)
连接使用的端口 (PORT)
TCP 连接发起的 IP 地址 (IP_SOURCE)
要运行该脚本,请执行
stap /usr/share/doc/packages/systemtap/examples/network/tcp_connections.stp
并关注屏幕上的输出。要手动停止该脚本,请按 Ctrl–C。
对于调试用户空间应用程序(就像 DTrace 可以做的那样),openSUSE Leap 15.6 支持使用 SystemTap 进行用户空间探测。可以在任何用户空间应用程序中插入自定义探测点。因此,SystemTap 允许您使用内核空间和用户空间探测来调试整个系统的行为。
要获得所需的 utrace 基础设施和用户空间探测的 uprobes 内核模块,您需要安装 kernel-trace 包,除了在 第 4.2 节,“安装和设置” 中列出的包之外。
utrace 实现了一个用于控制用户空间任务的框架。它提供了一个接口,可以被各种追踪 “引擎” 使用,这些引擎以可加载的内核模块形式实现。这些引擎会为特定事件注册回调函数,然后附加到他们想要追踪的线程。由于回调函数是从内核中的 “安全” 位置调用的,这允许函数执行各种处理操作。可以通过 utrace 观察多个事件。例如,你可以观察系统调用进入和退出、fork() 以及信号发送到任务等事件。关于 utrace 基础设施的更多细节,请访问 https://sourceware.org/systemtap/wiki/utrace。
SystemTap 包含支持探测用户空间进程中函数的进入和返回、探测用户空间代码中的预定义标记以及监控用户进程事件的功能。
要检查当前运行的内核是否提供了所需的 utrace 支持,请使用以下命令
>sudogrep CONFIG_UTRACE /boot/config-`uname -r`
有关用户空间探测的更多细节,请参考 https://sourceware.org/systemtap/SystemTap_Beginners_Guide/userspace-probing.html。
本章仅提供 SystemTap 的简要概述。有关 SystemTap 的更多信息,请参考以下链接
SystemTap 项目主页。
一个庞大的 SystemTap 有用信息集合,涵盖了从详细的用户和开发者文档到与其他工具的评论和比较,以及常见问题解答和技巧。还包含 SystemTap 脚本、示例和使用案例的集合,以及最近关于 SystemTap 的演讲和论文列表。
提供 SystemTap 教程、SystemTap 初学者指南、Tapset 开发者指南 和 SystemTap 语言参考,格式为 PDF 和 HTML。还列出了相关的 man 手册页。
您还可以从安装系统的 /usr/share/doc/packages/systemtap 目录中找到 SystemTap 语言参考和 SystemTap 教程。示例 SystemTap 脚本可从 example 子目录获得。