4 SystemTap — 过滤和分析系统数据 #
  SystemTap 提供了命令行界面和脚本语言,用于细致检查运行中 Linux 系统(特别是内核)的活动。SystemTap 脚本采用 SystemTap 脚本语言编写,随后会编译为 C 代码内核模块并插入到内核中。您可以设计脚本来提取、过滤和汇总数据,以便对复杂的性能问题或功能问题进行诊断。SystemTap 提供的信息与 netstat、ps、top 和 iostat 等工具的输出类似。不过,它提供了更多用于过滤和分析所收集信息的选项。
 
4.1 概念概述 #
   每当您运行 SystemTap 脚本时,都会启动一个 SystemTap 会话。需要针对脚本传递几个参数,脚本才能运行。然后,该脚本会编译为内核模块并加载到内核中。如果以前执行过该脚本,并且任何系统组件都未更改(例如,不同的编译器或内核版本、库路径或脚本内容),SystemTap 不会再次编译该脚本,而是使用 SystemTap 缓存 (~/.systemtap) 中存储的 *.c 和 *.ko 数据。
  
当 tap 完成运行时,将卸载模块。有关示例,请参见第 4.2 节 “安装和设置”中的测试运行和相关说明。
4.1.1 SystemTap 脚本 #
    SystemTap 的用法基于 SystemTap 脚本 (*.stp)。这些脚本会告知 SystemTap 要收集哪类信息,以及收集信息后要执行哪项操作。脚本采用与 AWK 和 C 类似的 SystemTap 脚本语言编写。有关语言定义,请访问 https://sourceware.org/systemtap/langref.pdf。您可以在 https://www.sourceware.org/systemtap/examples/ 中找到大量实用的示例脚本。
   
    SystemTap 脚本的基本运作原理是指定 events 并为其提供 handlers。当 SystemTap 运行脚本时,它会监控特定的事件。发生某个事件时,Linux 内核会将处理程序作为子例程运行,然后继续。因此,事件充当了运行处理程序的触发器。处理程序可以记录指定的数据,并以特定的方式列显数据。
   
SystemTap 语言仅使用少量几种数据(整数、字符串,以及这些类型的关联数组)和完整控制结构(块、条件、循环、函数)。它包含轻量标点符号(可选用分号),且不需要详细的声明(系统会自动推断并检查类型)。
    有关 SystemTap 脚本及其语法的详细信息,请参见第 4.3 节 “脚本语法”,以及 systemtap-docs 软件包中提供的 stapprobes 和 stapfuncs 手册页。
   
4.1.2 Tapset #
     是预先编写、可在 SystemTap 脚本中使用的探测和函数库。当用户运行某个 SystemTap 脚本时,SystemTap 会根据 tapset 库检查该脚本的探测事件和处理程序。然后,SystemTap 会先加载相应的探测和函数,然后再将脚本转换为 C。与 SystemTap 脚本本身类似,tapset 使用文件扩展名 *.stp。
   
但是,与 SystemTap 脚本不同的是,tapset 不可直接执行。它们构成了可供其他脚本提取定义的库。因此,tapset 库是一个抽象层,旨在方便用户定义事件和函数。Tapset 为用户可能想要作为事件指定的函数提供别名。了解正确的别名通常比记住特定的内核函数更容易(不同内核版本的内核函数可能会不同)。
4.1.3 命令和特权 #
    与 SystemTap 关联的主要命令包括 stap 和 staprun。要执行这些命令,您需要拥有 root 特权,或者必须是 stapdev 或 stapusr 组的成员。
   
- stap
- SystemTap 前端。运行 SystemTap 脚本(通过文件或标准输入)。此命令会将该脚本转换为 C 代码,并将生成的内核模块加载到正在运行的 Linux 内核中。然后执行请求的系统跟踪或探测函数。 
- staprun
- SystemTap 后端。加载和卸载 SystemTap 前端生成的内核模块。 
    如需每个命令的选项列表,请使用 --help。有关细节,请参见 stap 和 staprun 手册页。
   
 
    为了避免单纯出于让用户能够使用 SystemTap 的目的而向其授予 root 访问权限,请使用以下 SystemTap 组之一。SUSE Linux Enterprise Server 上默认未提供这些组,但您可以创建组,并相应地修改访问权限。另外,您还可调整 staprun 命令的权限,前提是这不会对您的环境安全产生不当影响。
   
- stapdev
- 此组的成员可以使用 - stap运行 SystemTap 脚本,或使用- staprun运行 SystemTap 工具模块。由于运行中的- stap涉及到将脚本编译为内核模块并将其加载到内核中,此组的成员仍拥有有效的- root访问权限。
- stapusr
- 此组的成员只能使用 - staprun运行 SystemTap 工具模块。此外,他们只能通过- /lib/modules/KERNEL_VERSION/systemtap/运行这些模块。此目录必须由- root拥有,且仅供- root用户写入。
4.1.4 重要文件和目录 #
以下列表概述了 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 代码和内核对象。 
4.2 安装和设置 #
   由于 SystemTap 需要内核相关信息,必须安装一些额外的内核相关软件包。对于您要使用 SystemTap 探测的每个内核,需要安装下面一组软件包。这组软件包应该与内核版本和变种(在下面的概述中以 * 表明)完全匹配。
  
    如果您为系统订阅了联机更新,便可以在 SUSE Linux Enterprise Server 15 SP7 的相关 *-Debuginfo-Updates 联机安装储存库中找到 “debuginfo” 软件包。使用 YaST 启用该储存库。
   
   对于经典 SystemTap 设置,请安装以下软件包(使用 YaST 或 zypper)。
  
- systemtap
- systemtap-server
- systemtap-docs(可选)
- kernel-*-base
- kernel-*-debuginfo
- kernel-*-devel
- kernel-source-*
- gcc
   要访问手册页和用于不同目的的有用示例 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.
| 
     根据任何所用 tapset 的  | |
| 检查脚本的各个组成部分。 | |
| 
     将脚本转换为 C。运行系统 C 编译器以基于脚本创建内核模块。生成的 C 代码 ( | |
| 
     在脚本中通过挂接到内核来加载模块并启用所有探测(事件和处理程序)。探测的事件是虚拟文件系统 (VFS) 读取操作。当该事件在任何处理器上发生时,一个有效的处理程序会执行(列显文本  | |
| 终止 SystemTap 会话后,已禁用探测并卸载内核模块。 | 
如果在测试期间出现了任何错误消息,请检查输出以获取有关缺少哪些软件包的提示,并确保正确安装这些软件包。另外,可能还需要重引导系统并加载适当内核。
4.3 脚本语法 #
SystemTap 脚本包含以下两个组成部分:
- SystemTap 事件(探测点)
- 为要执行的关联处理程序上的内核事件命名。事件的示例包括进入或退出特定的函数、计时器即将失效,或者启动或终止会话。 
- SystemTap 处理程序(探测主体)
- 用于指定每当发生特定事件时要执行操作的脚本语言语句系列。通常包括从事件环境提取数据、将数据存储到内部变量,或列显结果。 
   事件及其相应的处理程序统称为 probe。SystemTap 事件也称为probe
   points。探测的处理程序也称为probe
   body。
  
   您可以在 SystemTap 脚本中的任意位置插入不同样式的注释:使用 #、/* */ 或 // 作为标记。
  
4.3.1 探测格式 #
一个 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 脚本中的语句块使用的语法和语义与 C 编程语言中使用的相同。
4.3.2 SystemTap 事件(探测点) #
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 手册页。该手册页的 See
    Also 部分还包含了其他手册页的链接,其中讨论了特定子系统和组件的支持事件。
   
4.3.3 SystemTap 处理程序(探测主体) #
每个 SystemTap 事件附带一个对应的处理程序,该处理程序是为该事件定义的,包含一个语句块。
4.3.3.1 函数 #
     如果您需要在多个探测中使用相同的语句集,可将这些语句放到一个函数中,以方便重复使用。函数是由关键字 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 函数如下:
- tid()
- 当前线程的 ID。 
- pid()
- 当前线程的进程 ID。 
- uid()
- 当前用户的 ID。 
- cpu()
- 当前 CPU 编号。 
- execname()
- 当前进程的名称。 
- gettimeofday_s()
- 自 Unix 纪元(1970 年 1 月 1 日)起经过的秒数。 
- ctime()
- 将时间转换为字符串。 
- pp()
- 用于描述当前正在处理的探测点的字符串。 
- thread_indent()
- 用于组织列显结果的有用函数。它会(在内部)存储每个线程 ( - tid()) 的缩进计数器。该函数接受一个参数,即缩进增量,用于指示要在线程的缩进计数器中添加或去除多少个空格。它会返回一个字符串,其中包含一些泛型跟踪数据,以及相应的缩进空格数。返回的泛型数据包括时戳(自线程初始缩进以来的微秒数)、进程名称和线程 ID 本身。这样您便可以识别调用了哪些函数、谁调用了这些函数,以及花费了多长时间。- 调用入口和出口通常不会彼此紧靠(否则很容易匹配)。在第一个调用入口及其出口之间,通常还创建了其他调用入口和出口。缩进计数器可帮助您将一个入口与对应的出口进行匹配,当后续函数调用并非前一个函数的出口时,缩进计数器会对该后续函数调用进行缩进。 
     有关支持的 SystemTap 函数的详细信息,请参见 stapfuncs 手册页。
    
4.3.3.2 其他基本构造 #
     除函数外,您还可以在 SystemTap 处理程序中使用其他常见构造,包括变量、条件语句(例如 if/else、while 循环、for 循环)、数组或命令行参数。
    
4.3.3.2.1 变量 #
可在脚本中的任意位置定义变量。要定义变量,只需选择一个名称,并通过函数或表达式为其赋值:
foo = gettimeofday( )
      然后便可以在表达式中使用该变量。SystemTap 根据变量的赋值类型自动推断每个标识符的类型(字符串或数字)。任何不一致性都将报告为错误。在上述示例中,foo 将自动分类为数字,可通过 printf() 并结合使用整数格式说明符 (%d) 进行列显。
     
      不过,默认情况下,变量位于包含它们的探测本地。系统每次调用处理程序时,会初始化、使用并处置这些变量。要在两个探测之间共享变量,请在脚本中的任意位置将其声明为全局变量。为此,请在探测的外部使用 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 ()
  }
       此示例脚本使用用于统计 jiffy 和毫秒数然后进行相应计算的计时器来计算内核的 CONFIG_HZ 设置。(Jiffy 是指一次系统计时器中断的持续时间。它不是一个绝对时间间隔单位,因为其持续时间取决于特定硬件平台的时钟中断频率)。借助 global 语句,还可以在探测 timer.ms(12345) 中使用变量 count_jiffies 和 count_ms。指定 ++ 时,变量值将会加 1。
      
4.3.3.2.2 条件语句 #
可以在 SystemTap 脚本中使用多种条件语句。最常见的条件语句如下:
- if/else 语句
- 这些语句使用以下格式表达: - if (CONDITION)1STATEMENT12 else3STATEMENT24 - if语句将整数值表达式与零进行比较。如果条件表达式 1 不为零,则执行第一个语句 2。如果条件表达式为零,则执行第二个语句 4。else 子句(3 和 4)是可选子句。2 和 4 也可以是语句块。
- While 循环
- 这些语句使用以下格式表达: - while (CONDITION)1STATEMENT2 - 当 - condition不为零时,将执行语句 2。2 也可以是一个语句块。它必须更改某个值,以便- condition最终变为零。
- For 循环
- 这些语句是 - while循环的快捷方式,使用以下格式表达:- for (INITIALIZATION1; CONDITIONAL2; INCREMENT3) statement - 1 中指定的表达式用于初始化循环迭代次数的计数器,在开始执行循环之前执行。循环执行将持续到循环条件 2 为 false。(在每个循环迭代的开头检查此表达式)。3 中指定的表达式用于递增循环计数器。它在每个循环迭代的末尾执行。 
- 条件运算符
- 可在条件语句中使用以下运算符: - ==: 等于 - !=: 不等于 - >=: 大于等于 - <=: 小于等于 
4.4 示例脚本 #
   如果您已安装 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。
4.5 用户空间探测 #
为帮助调试用户空间应用程序(就像 DTrace 可做到的那样),SUSE Linux Enterprise Server 15 SP7 支持使用 SystemTap 进行用户空间探测。在任何用户空间应用程序中都可插入自定义探测点。因此,通过 SystemTap,您可以使用内核空间和用户空间探测来调试整个系统的行为。
   要获取所需的 utrace 基础架构和 uprobes 内核模块进行用户空间探测,除了第 4.2 节 “安装和设置”中所列的软件包外,还需要安装 kernel-trace 软件包。
  
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。
4.6 更多信息 #
本章仅提供了简短的 SystemTap 概述。有关 SystemTap 的详细信息,请参考以下链接:
- https://sourceware.org/systemtap/
- SystemTap 项目主页。 
- https://sourceware.org/systemtap/wiki/
- 囊括了有关 SystemTap 的大量有用信息,从详细的用户和开发人员文档,到评论以及与其他工具的比较,或者常见问题和提示。另外还包含 SystemTap 脚本、示例和使用案例集合,并列出了有关 SystemTap 的最近研讨主题和论文。 
- https://sourceware.org/systemtap/documentation.html
- 提供 PDF 和 HTML 格式的 SystemTap Tutorial、SystemTap Beginner's Guide、Tapset Developer's Guide 和 SystemTap Language Reference。另外还列出了相关的手册页。 
   您还可以在所安装系统中的 /usr/share/doc/packages/systemtap 下找到 SystemTap 语言参考和 SystemTap 教程。example 子目录中提供了示例 SystemTap 脚本。