Solaris 动态跟踪指南

第 1 章 简介

欢迎使用 Solaris 操作系统的动态跟踪工具!如果您想了解您的系统的行为,那么 DTrace 正是您所需要的工具。DTrace 是一个内置于 Solaris 中的全面的动态跟踪工具。管理员和开发者可以使用该工具,在实时生产系统上检查用户程序及操作系统本身的行为。DTrace 允许您查看系统,以便了解其工作方式、在软件的多个层之间跟踪性能问题或找出导致异常行为的原因。如您所见,可以使用 DTrace 来创建自己的自定义程序,以便动态地检测系统,并对可用 DTrace D 编程语言阐明的任意问题作出快速简明的回答。本章第一部分对 DTrace 进行了快速介绍,并说明如何编写第一个 D 程序。本章其余部分则介绍了采用 D 语言编程的全套规则,以及用于对系统进行深入分析的技巧及方法。您可以在 Web 站点(网址为 http://www.sun.com/bigadmin/content/dtrace/)上与其他 DTrace 社区人员共享您 DTrace 经验和脚本。本指南中提供的所有示例脚本均可在 Solaris 系统的 /usr/demo/dtrace 目录中找到。

入门

使用 DTrace,可以动态修改操作系统内核和用户进程,以记录您在所关注的位置(称为探测器)指定的其他数据,从而帮助您了解软件系统。探测器是一个位置或活动,DTrace 可以将请求绑定到该位置或活动中,以便执行一组操作,如记录栈跟踪、时间标记或函数参数。探测器类似于分散在整个 Solaris 系统中受关注位置的可编程传感器。如果要确定系统的状态,可使用 DTrace 对相应的传感器进行编程,以便记录您关注的信息。然后,当每个探测器触发时,DTrace 便会从探测器中收集数据并向您报告。如果未为探测器指定任何操作,DTrace 将仅记录探测器的每次触发。

在 DTrace 中,每个探测器都有两个名称:一个唯一的整数 ID 以及一个人工可读的字符串名称。接下来,我们将通过使用名为 BEGIN 的探测器生成一些非常简单的请求来开始学习 DTrace,该探测器在每次启动新的跟踪请求时触发一次。您可以使用 dtrace(1M) 实用程序的 -n 选项,通过探测器的字符串名称来启用探测器。键入以下命令:


# dtrace -n BEGIN

在简短暂停后,DTrace 将会通知您已启用一个探测器,并且会显示一行输出,表明 BEGIN 探测器已被触发。显示此输出后,dtrace 将保持暂停状态,以等待其他探测器触发。由于尚未启用任何其他探测器,并且 BEGIN 仅触发一次,因此可在 shell 中按 Ctrl-C 组合键,退出 dtrace 并返回到 shell 提示符:


# dtrace -n BEGIN
dtrace: description 'BEGIN' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
#

该输出表明,名为 BEGIN 的探测器已被触发一次,并且还会列显其名称和整数 ID 1。请注意,缺省情况下会显示触发此探测器的 CPU 的整数名称。在此示例中,CPU 列表明触发该探测器时,正在 CPU 0 上执行 dtrace 命令。

您可以使用任意数量的探测器和操作构造 DTrace 请求。通过在上一示例命令中添加 END 探测器,我们来使用两个探测器创建一个简单的请求。END 探测器在完成跟踪时触发一次。请键入以下命令,然后在显示 BEGIN 探测器的输出行后,再次在 shell 中按 Ctrl-C 组合键:


# dtrace -n BEGIN -n END
dtrace: description 'BEGIN' matched 1 probe
dtrace: description 'END' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0      1                  :BEGIN
^C
  0      2                    :END
#

如您所见,按 Ctrl-C 组合键退出 dtrace 时可触发 END 探测器。dtrace 在退出之前,会报告 此探测器的触发情况。

至此,您已大致了解了如何命名和启用探测器,现在便可以尝试编写最简单的 DTrace 版程序 "Hello, World"。除了可在命令行中构造 DTrace 实验脚本之外,还可使用 D 编程语言在文本文件中进行编写。在文本编辑器中,创建一个称作 hello.d 的新文件,然后键入您的第一个 D 程序:


示例 1–1 hello.d:采用 D 编程语言编写的 "Hello, World"

BEGIN
{
	trace("hello, world");
	exit(0);
}

保存程序后,可以使用 dtrace -s 选项运行该程序。键入以下命令:


# dtrace -s hello.d
dtrace: script 'hello.d' matched 1 probe
CPU     ID		      FUNCTION:NAME
  0	    1                  :BEGIN   hello, world
#

如您所见,dtrace 显示的输出与前面相同,后跟文本 "hello, world"。与上例不同的是,您不需要等待,也不需要按 Ctrl-C 组合键。这些更改是在 hello.d 中为 BEGIN 探测器指定的操作的结果。为了解所发生的情况,让我们来看看 D 程序的详细结构。

每个 D 程序均由一系列子句组成,每个子句用于描述一个或多个要启用的探测器,以及触发探测器时要执行的一组可选操作。这些操作以一系列括在大括号 { }(跟在探测器名称后面)中的语句形式列出。每条语句都以分号 (;) 结尾。您的第一条语句使用函数 trace() 来指示 DTrace 应在触发 BEGIN 探测器时记录指定的参数(字符串 "hello, world"),然后将其显示出来。第二条语句使用函数 exit() 来指示 DTrace 应停止跟踪并退出 dtrace 命令。DTrace 提供了一组类似于 trace()exit() 的有用函数,供您在 D 程序中调用。要调用函数,请指定函数名称,并后跟用括号括起的参数列表。有关全套 D 函数的描述,请参阅第 10 章

如果您熟悉 C 编程语言,则此时您可能已从名称和示例了解到,DTrace 的 D 编程语言与 C 编程语言非常类似。实际上,D 源自一个大型的 C 子集和一组可简化跟踪的特定函数和变量。在后续章节中,您将会进一步了解这些功能。如果您以前编写过 C 程序,则可将大部分知识立即运用到 D 中生成跟踪程序。如果您以前从未编写过 C 程序,学习 D 也很容易。经过本章的学习后,您将了解所有语法。不过,让我们首先从语言规则开始,进一步了解 DTrace 的工作方式,然后再返回来学习如何生成更有趣的 D 程序。

提供器和探测器

在前面的示例中,您学习了如何使用两个简单的、名为 BEGINEND 的探测器。但是,这些探测器来自何处呢?DTrace 探测器来自一组称为提供器的内核模块,其中,每个模块都执行一种特定类型的检测过程来创建探测器。使用 DTrace 时,每个提供器均有机会将可提供的探测器发布到 DTrace 框架。然后,您可以启用并将跟踪操作绑定到已发布的任何探测器。要列出系统中的所有可用探测器,请键入以下命令:


# dtrace -l
  ID   PROVIDER            MODULE          FUNCTION NAME
   1     dtrace                                     BEGIN
   2     dtrace                                     END
   3     dtrace                                     ERROR
   4   lockstat           genunix       mutex_enter adaptive-acquire
   5   lockstat           genunix       mutex_enter adaptive-block
   6   lockstat           genunix       mutex_enter adaptive-spin
   7   lockstat           genunix       mutex_exit  adaptive-release

   ... many lines of output omitted ...

#

显示所有输出可能需要一些时间。要统计所有探测器的数量,可以键入以下命令:


# dtrace -l | wc -l
        30122

您可能会注意到您的计算机中的探测器总数有所不同,这是因为探测器的数量因操作平台及所安装的软件而异。如您所见,有大量的探测器可供您使用,因此,您可以检查系统中以前未曾关注过的的每个位置。事实上,如您随后将了解到的,即使此输出列表也是不完整的,因为某些提供器可基于跟踪请求动态创建新的探测器,从而导致 DTrace 探测器的实际数量是无限。

现在,我们返回来在终端窗口中查看 dtrace -l 的输出。请注意先前已经提到过的,每个探测器都有两个名称,一个整数 ID 以及一个人工可读的名称。人工可读的名称包括四个部分,如 dtrace 输出中的各列所示。探测器名称的四个部分如下:

提供器 

发布此探测器的 DTrace 提供器的名称。提供器名称通常与执行检测过程以启用探测器的 DTrace 内核模块的名称相对应。 

模块 

此探测器对应于特定的程序位置时,为探测器所在模块的名称。该名称为内核模块的名称或用户库的名称。 

功能 

此探测器对应于特定的程序位置时,为探测器所在程序函数的名称。 

名称 

探测器名称的最后组成部分是一个有助于您了解探测器语义的名称(如 BEGINEND)。

写出完整的人工可读的探测器名称时,请用冒号分隔列出该名称的所有四个部分,如下所示:

提供器:模块:函数:名称

请注意,该列表中某些探测器没有模块和函数,如先前使用的 BEGINEND 探测器。某些探测器将这两个字段留为空白,这是因为这些探测器与任何特定的受检测程序函数或位置都不对应。相反,这些探测器是指一个更抽象的概念,如跟踪请求结束。将模块和函数作为其名称一部分的探测器称为固定探测器,名称中没有模板和函数的探测器则称为不固定探测器

根据约定,如果没有指定探测器名称的所有字段,则只要探测器中的值与您指定的那部分名称相匹配,DTrace 就会将您的请求与所有此类探测器匹配。换而言之,在先前使用探测器名称 BEGIN 时,您实际上是指示 DTrace 匹配名称字段为 BEGIN 的任何探测器,而不考虑提供器、模块和函数字段的具体值。由于恰好只有一个探测器与该描述相符,因此才出现相同的结果。但是,您现在知道 BEGIN 探测器的实际名称为 dtrace:::BEGIN,这表示该探测器由 DTrace 框架本身提供,并且不固定到任何函数。因此,如果按以下所示方式编写 hello.d 程序,则也会生成相同结果:

dtrace:::BEGIN
{
	trace("hello, world");
	exit(0);
}

至此,您已了解探测器的来源和命名方式,接下来我们将进一步了解启用探测器并要求 DTrace 执行某些操作时发生的情况,然后我们会返回到 D 程序旋风之旅。

编译和检测过程

在 Solaris 中编写传统程序时,可使用编译器将程序从源代码转换为可执行的目标代码。使用 dtrace 命令时,将调用先前用于编写 hello.d 程序的 D 语言的编译器。程序编译完毕后,随即发送到操作系统内核中以供 DTrace 执行。在操作系统内核中,将启用程序中指定的探测器,并且对应的提供器会执行激活探测器所需的任何检测过程。

DTrace 中的所有检测过程都是完全动态的:仅在需要使用时分别启用所需的探测器。对于未激活的探测器,不存在任何检测代码,因此在不使用 DTrace 时,系统的性能丝毫不会降低。一旦完成实验脚本并退出 dtrace 命令,便会自动禁用使用的所有探测器,并取消探测器检测过程,从而将系统彻底返回至初始状态。在未激活 DTrace 的系统和未安装 DTrace 软件的系统之间,差异并不明显。

每个探测器的检测过程都在实时运行的操作系统或所选的用户进程上动态地执行。该系统不会以任何方式停顿或暂停,并且仅为启用的探测器添加检测代码。因此,使用 DTrace 所产生的探测影响被准确限制在要求 DTrace 执行的操作上:不会跟踪任何无关的数据,也不会在系统中打开一个大型“跟踪开关”,所有 DTrace 检测过程的设计原则都是尽可能提高效率。通过这些功能,可在生产中使用 DTrace 来实时解决实际问题。

DTrace 框架还提供对任意数量的虚拟客户机的支持。在系统内存容量允许的情况下,您可以同时运行任意数目的 DTrace 实验脚本和命令,并且所有命令都使用同一基本检测过程独立运行。这一功能还可使系统中任意数目的用户同时使用 DTrace:使用 DTrace,开发者、管理员和服务人员可以在同一系统中合作,也可单独处理同一系统中的不同问题,而不会相互干扰。

DTrace D 程序会被编译成一种安全的中间格式,用于在触发探测器时执行,这一点与 C 和 C++ 编写的程序不同,但与 JavaTM 编程语言编写的程序类似。当 DTrace 内核软件对程序进行首次检查时,将会验证该中间格式是否安全。DTrace 执行环境还可处理在 D 程序执行期间可能发生的任何运行时错误,包括除数为零、取消引用无效内存等,并将这些错误向您报告。因此,您永远不会构造出可能使 DTrace 意外损坏 Solaris 内核或系统中运行的某个进程的不安全程序。通过这些安全功能,可在生产环境中使用 DTrace,而不用担心系统崩溃或损坏。如果出现编程错误,DTrace 将向您报告该错误并禁用检测过程。然后,您可以纠正相应错误并重试。有关 DTrace 错误报告和调试功能,请参见本书后续章节。

下图说明了 DTrace 体系结构的各个组件,包括提供器、探测器、DTrace 内核软件和 dtrace 命令。

图 1–1 DTrace 体系结构和组件概述

DTrace 体系结构:内核工具和提供器、从内核到库的驱动程序接口,以及支持一组命令的库。

至此,您已了解 DTrace 的工作方式,现在让我们返回来学习 D 编程语言,并开始编写一些更有趣的程序。

变量和算术表达式

接下来的示例程序使用 DTrace profile 提供器实现基于时间的简单计数器。该 profile 提供器可以根据 D 程序中的描述创建新的探测器。如果为某个整数 n 创建一个名为 profile:::tick-n sec 的探测器,该 profile 提供器将创建一个触发间隔为 n 秒的探测器。键入以下源代码,并将其保存在名为 counter.d 的文件中:

/*
 * Count off and report the number of seconds elapsed
 */
dtrace:::BEGIN
{
	i = 0;
}

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

dtrace:::END
{
	trace(i);
}

执行时,程序将统计已用的秒数,直到按下 Ctrl-C 组合键为止,然后在结尾显示总计:


# dtrace -s counter.d
dtrace: script 'counter.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
  0  25499                       :tick-1sec         1
  0  25499                       :tick-1sec         2
  0  25499                       :tick-1sec         3
  0  25499                       :tick-1sec         4
  0  25499                       :tick-1sec         5
  0  25499                       :tick-1sec         6
^C
  0      2                             :END         6
#

程序的前三行为注释,用于解释程序将执行的操作。与 C、C++、Java 编程语言类似,D 编译器会忽略 /**/ 符号之间的任何字符。可以在 D 程序中的任何位置使用注释,包括探测器子句的内部和外部。

BEGIN 探测器子句定义了一个名为 i 的新变量,并使用以下语句将整数值零赋给该变量:

i = 0;

与 C、C++ 和 Java 编程语言不同,只需在程序语句中使用便可创建 D 变量,而无需进行显式变量声明。在程序中首次使用某个变量时,将会根据变量的第一个赋值的类型设置其类型。在程序的生命周期中,每个变量仅有一个类型,因此后续引用的类型必须与初始赋值的类型相同。在 counter.d 中,变量 i 首先被赋值为整型常数零,因此其类型设置为 int。D 提供了与 C 相同的基本整数数据类型,包括:

char

字符或单字节整数 

int

缺省整数 

short

短整数 

long

长整数 

long long

扩展的长整数 

这些类型的大小取决于操作系统内核的数据模型,如第 2 章中所述。D 还为各种固定大小的带符号整数类型和无符号整数类型以及由操作系统定义的大量其他类型提供了内置的友好名称。

counter.d 的中心部分是使计数器 i 递增的探测器子句:

profile:::tick-1sec
{
	i = i + 1;
	trace(i);
}

此子句将探测器命名为 profile:::tick-1sec,以指示 profile 提供器创建一个在可用处理器中每秒触发一次的新探测器。该子句包括两条语句,第一条语句指定 i 的值为前一个值加一,第二条语句跟踪 i 的新值。所有常见的 C 算术运算符均可用于 D 程序;在第 2 章中可以找到完整的列表。与 C 中一样,++ 运算符也可以用作以一为增量递增相应变量的简写。trace() 函数接受任何 D 表达式作为其参数,因此可以更简明地编写 counter.d,如下所示:

profile:::tick-1sec
{
	trace(++i);
}

如果要显式控制变量 i 的类型,可以在为变量赋值时将所需类型括在括号中,以便将整数零强制转换为特定类型。例如,如果要确定 D 中 char 的最大大小,可以按照如下所示更改 BEGIN 子句:

dtrace:::BEGIN
{
	i = (char)0;
}

在运行 counter.d 一段时间后,应看到跟踪的值增大,然后绕回为零。如果您没有耐心等待值绕回,可尝试将 profile 探测器名称更改为 profile:::tick-100msec,使计数器每 100 毫秒递增一次或每秒递增 10 次。

谓词

D 语言和其他编程语言(如 C、C++ 和 Java 编程语言)之间的一个主要差别是没有控制流结构(如 if 语句和循环)。D 程序子句以单个直线语句列表的形式编写,用于跟踪固定数量的可选数据。D 使用称为谓词的逻辑表达式,提供了根据条件跟踪数据和修改控制流的功能,谓词可用于为程序子句添加前缀。触发探测器时,在执行与相应的子句关联的任何语句之前,将计算谓词表达式。如果谓词计算结果为 true(由任何非零值表示),则执行语句列表。如果谓词计算结果为 false(由零值表示),则不执行任何语句并忽略探测器触发。

为下一个示例键入以下源代码,并将其保存在名为 countdown.d 的文件中:

dtrace:::BEGIN
{
	i = 10;
}

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

此 D 程序使用谓词实现一个 10 秒的递减计数计时器。执行时,countdown.d 从 10 开始递减计数,然后显示一条消息并退出:

# dtrace -s countdown.d
dtrace: script 'countdown.d' matched 3 probes
CPU     ID                    FUNCTION:NAME
	0  25499                       :tick-1sec        10
	0  25499                       :tick-1sec         9
	0  25499                       :tick-1sec         8
	0  25499                       :tick-1sec         7
	0  25499                       :tick-1sec         6
	0  25499                       :tick-1sec         5
	0  25499                       :tick-1sec         4
	0  25499                       :tick-1sec         3
	0  25499                       :tick-1sec         2
	0  25499                       :tick-1sec         1
	0  25499                       :tick-1sec   blastoff!
# 

此示例使用 BEGIN 探测器将整数 i 初始化为 10 以开始递减计数。接下来,与前一个示例相同,程序使用 tick-1sec 探测器实现每秒触发一次的计时器。请注意,在 countdown.d 中,tick-1sec 探测器描述用在两条不同的子句中,每条子句使用一个不同的谓词和操作列表。谓词是置于封闭斜杠 / / 之间的逻辑表达式,出现在探测器名称之后、括住子句语句列表的大括号 { } 之前。

第一个谓词测试 i 是否大于零,以指示计时器是否仍在运行:

profile:::tick-1sec
/i > 0/
{
	trace(i--);
}

关系运算符 > 表示大于,如果为 false 则返回整数值零,如果为 true 则返回一。D 中支持所有 C 关系运算符;在第 2 章中可以找到完整的列表。如果 i 不为零,则脚本将跟踪 i,然后使用 -- 运算符以一为减量递减。

如果 i 恰好等于零,则第二个谓词使用 == 运算符返回 true,指示递减计数已完成:

profile:::tick-1sec
/i == 0/
{
	trace("blastoff!");
	exit(0);
}

当递减计数完成时,hello.dcountdown.d 使用称为字符串常量、引在双引号中的一个字符序列来显示最终消息,这一点与第一个示例类似。然后使用 exit() 函数退出 dtrace 并返回到 shell 提示符。

如果返回来查看 countdown.d 的结构,将会看到通过创建两个具有相同的探测器描述但具有不同谓词和操作的子句,有效地创建了以下逻辑流:

i = 10
once per second,
	if i is greater than zero
		trace(i--);
	otherwise if i is equal to zero
		trace("blastoff!");
		exit(0);

如果您希望使用谓词编写复杂的程序,请首先尝试按照此方式将您的算法直观化,然后将条件结构的每个路径转换为独立的子句和谓词。

现在,我们将谓词与一个新的提供器(syscall 提供器)组合在一起,创建第一个真实的 D 跟踪程序。syscall 提供器允许在进入任何 Solaris 系统调用或从该系统调用返回时启用探测器。以下示例使用 DTrace 对 shell 执行的每次 read(2)write(2) 系统调用进行观察。首先打开两个终端窗口,一个用于 DTrace,另外一个包含您将查看的 shell 进程。在第二个窗口中,键入以下命令以获取此 shell 的进程 ID:


# echo $$
12345

现在返回到第一个终端窗口并键入以下 D 程序,然后将其保存在名为 rw.d 的文件中。键入程序时,用响应 echo 命令列显的 shell 的进程 ID 替换 12345

syscall::read:entry,
syscall::write:entry
/pid == 12345/
{

}

请注意,由于该程序仅用于跟踪探测器触发通知而不用于跟踪任何其他数据,因此将 rw.d 的探测器子句的主体保留为空。完成 rw.d 键入后,使用 dtrace 开始编写实验脚本,然后转到第二个 shell 窗口键入一些命令,并在每个命令后按回车键。键入时,您应在第一个窗口中看到 dtrace 报告所出现的探测器触发,与以下示例类似:


# dtrace -s rw.d
dtrace: script 'rw.d' matched 2 probes
CPU     ID                    FUNCTION:NAME
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
	0     34                      write:entry 
	0     32                       read:entry 
...

您现在会注意到,shell 执行 read(2)write(2) 系统调用从终端窗口中读取字符并回显结果!此示例包括许多到现在为止已描述的概念和一些新概念。首先,要采用相同方式检测 read(2)write(2),脚本应使用包含多个探测器描述的单条探测器子句,并用逗号分隔这些描述,如下所示:

syscall::read:entry,
syscall::write:entry

为了获得较好的可读性,每个探测器描述均独占一行。当然,并非一定按此方式编写,但这样做可使脚本可读性更好。接下来,脚本定义一个谓词,该谓词仅与 shell 进程执行的那些系统调用匹配:

/pid == 12345/

该谓词使用预定义的 DTrace 变量 pid,其计算结果始终为与触发相应探测器的线程关联的进程 ID。DTrace 为各种有用的项目(如进程 ID)提供了许多内置的变量定义。以下是一些 DTrace 变量的列表,您可以使用这些变量来编写第一个 D 程序:

变量名 

数据类型 

含义 

errno

int

系统调用的当前 errno

execname

string

当前进程的可执行文件的名称 

pid

pid_t

当前进程的进程 ID 

tid

id_t

当前线程的线程 ID 

probeprov

string

当前探测器描述的提供器字段 

probemod

string

当前探测器描述的模块字段 

probefunc

string

当前探测器描述的函数字段 

probename

string

当前探测器描述的名称字段 

现在,您已经编写了一个实际的检测程序,可尝试通过更改要检测的进程 ID 和系统调用探测器,对系统上运行的不同进程试验该程序。然后,可以进行一次更简单的更改,将 rw.d 转变为一个非常简单的系统调用跟踪工具(如 truss(1))版本。空的探测器描述字段充当通配符,可与任何探测器匹配,因此将程序更改为以下新源代码可跟踪 shell 执行的任何系统调用:

syscall:::entry
/pid == 12345/
{

}

尝试在 shell 中键入一些命令(如 cdlsdate),然后查看 DTrace 程序报告的内容。

输出格式化

系统调用跟踪是一种观察大多数用户进程行为的有效方法。如果您以前以管理员或开发者身份使用过 Solaris truss(1) 实用程序,则可能已了解到,该实用程序是一种用于解决问题的有用工具。如果以前从未使用过 truss,则可在某个 shell 中键入以下命令立即进行尝试:


$ truss date

您将会看到 date(1) 执行的所有系统调用的格式化跟踪,结尾紧跟一行输出。以下示例对前面的 rw.d 程序进行了改进,将其输出格式设置为与 truss(1) 更相似,从而便于您理解输出。键入以下程序,并将其保存在名为 trussrw.d 的文件中:


示例 1–2 trussrw.d:使用 truss(1) 输出格式跟踪系统调用

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

在此示例中,将每个谓词中的常数 12345 替换为标签 $1。使用此标签可将所关注的进程指定为脚本参数:编译脚本时,将用第一个参数的值替换 $1。要执行 trussrw.d,请使用 dtrace 选项 -q-s,后跟 shell 的进程 ID 作为最后一个参数。-q 选项指出,dtrace 应为静默模式,并取消前面示例中显示的标题行和 CPU 及 ID 列。因此,您将仅会看到显式跟踪的数据的输出。键入以下命令(将 12345 替换为 shell 进程的进程 ID),然后在指定的 shell 中多次按回车键。


# dtrace -q -s trussrw.d 12345
	                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)                 = 1
write(2, 0x8089e48,    1)                = 1
read(63, 0x8090a38, 1024)                = 0
read(63, 0x8090a38, 1024)                = 0
write(2, 0x8089e48,   52)                = 52
read(0, 0x8089878,    1)^C
#

现在我们来更详细地检查 D 程序及其输出。首先,与前面的程序类似的一条子句检测 shell 的每次 read(2)write(2) 调用。但对于此示例,将使用一个新函数 printf() 来跟踪数据并以特定格式显示数据:

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);
}

printf() 函数将跟踪数据的功能(就像先前使用的 trace() 函数一样)与以描述的特定格式输出数据和其他文本的功能组合在一起。printf() 函数会指示 DTrace 跟踪与第一个参数之后的每个参数关联的数据,然后使用第一个 printf() 参数说明的规则(称为格式字符串)来格式化结果。

格式字符串是包含任意数量的格式转换的规则字符串,其中的每个转换都以 % 字符开头,该字符说明如何设置对应的参数格式。格式字符串中的第一个转换对应于第二个 printf() 参数,第二个转换对应于第三个参数,依此类推。转换之间的所有文本将逐字列显。% 转换字符之后的字符说明要用于对应参数的格式。以下是 trussrw.d 中使用的三种格式转换的含义:

%d

将相应的值显示为十进制整数 

%s

将相应的值显示为字符串 

%x

将相应的值显示为十六进制整数 

DTrace printf() 的工作方式与 C printf(3C) 库例程或 shell printf(1) 实用程序类似。如果您以前对 printf() 完全不了解,请参阅第 12 章中对格式和选项的详细说明。即使您通过其他语言已经熟悉了 printf(),也应该认真阅读本章。在 D 中,将 printf() 作为内置功能提供,并且专为 DTrace 设计了一些新的格式转换供您使用。

为了帮助您编写正确的程序,D 编译器将会针对参数列表验证每个 printf() 格式字符串。请尝试将以上子句中的 probefunc 更改为整数 123。如果运行修改的程序,将会显示一条错误消息,通知您字符串格式转换 %s 不适用于整数参数:


# dtrace -q -s trussrw.d
dtrace: failed to compile script trussrw.d: line 4: printf( )
	   argument #2 is incompatible with conversion #1 prototype:
	        conversion: %s
	         prototype: char [] or string (or use stringof)
	          argument: int
#

要显示读或写系统调用及其参数的名称,请使用 printf() 语句:

printf("%s(%d, 0x%x, %4d)", probefunc, arg0, arg1, arg2);

以跟踪当前探测器函数和系统调用的前三个整数参数(可从 DTrace 变量 arg0arg1arg2 中获得)的名称。有关探测器参数的更多信息,请参见第 3 章read(2)write(2) 的第一个参数为文件描述符,以十进制格式显示。第二个参数为缓冲区地址,格式为十六进制值。最后一个参数为缓冲区大小,格式为十进制值。格式说明符 %4d 用于第三个参数,指示应使用 %d 格式转换显示该值,最小字段宽度为 4 个字符。如果该整数的宽度小于 4 个字符,printf() 将插入额外的空白以对齐输出。

要显示系统调用的结果并完成每一行输出,使用以下子句:

syscall::read:return,
syscall::write:return
/pid == $1/
{
	printf("\t\t = %d\n", arg1);
}

请注意,除 entry 外,syscall 提供器还为每个系统调用发布了一个名为return 的探测器。在 syscall return 探测器中,DTrace 变量 arg1 保存了系统调用的返回值。返回值的格式为十进制整数。格式字符串中以反斜杠开头的字符序列分别扩展为制表符 (\t) 和换行符 (\n)。这些转义序列可帮助您显示或记录难以键入的字符。D 支持与 C、C++ 和 Java 编程语言相同的一组转义序列。完整的转义序列列表请参阅第 2 章

数组

D 允许您定义整数和其他类型的变量,以便表示字符串和称为结构联合的复合类型。如果您熟悉 C 编程,那么应该很高兴,因为您可以在 D 中使用 C 中使用的任何类型。如果您不是 C 语言专家,也不要担心:第 2 章中描述了所有不同类型的数据类型。D 还支持一种称为关联数组的特殊类型的变量。关联数组与常规数组类似,因为它也将一组键与一组值相关联,但是在关联数组中,键不限于固定范围的整数。

D 关联数组可以由任何类型的一个或多个值的列表建立索引。各个键值一起构成元组,用于对数组建立索引以及访问或修改与该键对应的值。与给定关联数组一起使用的每个元组必须符合相同类型的签名;也就是说,每个元组键的长度必须相同,且具有同样顺序的相同键类型。整个数组中与给定关联数组的每个元素关联的值也是同一个固定类型。例如,以下 D 语句使用元组签名 [ string, int ] 定义一个 int 值类型的新关联数组 a,并将整数值 456 存储在该数组中:

a["hello", 123] = 456;

在定义数组之后,可以像访问任何其他 D 变量一样访问数组的元素。例如,以下 D 语句通过将值从 456 递增到 457 来修改 a 中先前存储的数组元素:

a["hello", 123]++;

尚未赋值的任何数组元素的值均设置为零。现在,我们学习在 D 程序中使用关联数组。键入以下程序,并将其保存在名为 rwtime.d 的文件中:


示例 1–3 rwtime.d:时间 read(2) 和 write(2) 调用

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

trussrw.d 相同,执行 rwtime.d 时,指定 shell 进程的 ID。如果键入一些 shell 命令,您将会看到每个系统调用过程中已用的时间。在其他 shell 中键入以下命令,然后多次按回车键:


# dtrace -s rwtime.d `pgrep -n ksh`
dtrace: script 'rwtime.d' matched 4 probes
CPU     ID                    FUNCTION:NAME
  0     33                      read:return 22644 nsecs
  0     33                      read:return 3382 nsecs
  0     35                     write:return 25952 nsecs
  0     33                      read:return 916875239 nsecs
  0     35                     write:return 27320 nsecs
  0     33                      read:return 9022 nsecs
  0     33                      read:return 3776 nsecs
  0     35                     write:return 17164 nsecs
...
^C
#

要跟踪每个系统调用的已用时间,必须对进入 read(2)write(2) 以及从它们返回的状态进行检测,并对每个点进行时间采样。然后,在从给定的系统调用返回时,必须计算第一个时间标记和第二个时间标记之间的差异。对于每个系统调用可以使用独立的变量,但这可能会使程序在扩展到其他系统调用时变得很乱。相反,使用由探测器函数名称建立索引的关联数组则简单得多。以下是第一个探测器子句:

syscall::read:entry,
syscall::write:entry
/pid == $1/
{
	ts[probefunc] = timestamp;
}

此子句定义一个名为 ts 的数组,并将 DTrace 变量 timestamp 的值赋给相应的成员。此变量返回一个始终递增的纳秒计数器的值,与 Solaris 库例程 gethrtime(3C) 类似。保存进入时间标记后,对应的返回探测器将再次对 timestamp 采样,并报告当前时间与所保存值之间的差异:

syscall::read:return,
syscall::write:return
/pid == $1 && ts[probefunc] != 0/
{
	printf("%d nsecs", timestamp - ts[probefunc]);
}

返回探测器中的谓词要求,DTrace 正在跟踪相应的进程,且对应的 entry 探测器已触发,并为 ts[probefunc] 指定了一个非零值。此技巧在首次启动 DTrace 时可以消除无效的输出。如果在执行 dtrace 时,shell 已在 read(2) 系统调用中等待输入,将会触发 read:return 探测器,但前面的第一个 read(2)read:entry 不会触发,且 ts[probefunc] 的计算结果将为零,因为尚未对其赋值。

外部符号和类型

DTrace 检测过程在 Solaris 操作系统内核内部执行,因此除了访问特殊 DTrace 变量和探测器参数外,您还可以访问内核数据结构、符号和类型。通过这些功能,高级 DTrace 用户、管理员、服务人员和驱动程序开发者可以检查操作系统内核和设备驱动程序的低级行为。在本书开头的阅读列表中,列出了可以帮助您进一步了解 Solaris 操作系统内核的书籍。

D 使用反引号字符 (`) 作为访问操作系统(而非 D 程序)中所定义的符号的特殊作用域运算符。例如,Solaris 内核包含一个名为 kmem_flags 的系统可调参数的 C 声明,用于启用内存分配器调试功能。有关 kmem_flags 的更多信息,请参见《Solaris Tunable Parameters Reference Manual》。此可调参数在内核源代码中使用 C 语言声明,如下所示:

int kmem_flags;

要在 D 程序中跟踪此变量的值,可以编写以下 D 语句:

trace(`kmem_flags);

DTrace 将每个内核符号与在对应操作系统 C 代码中用于该符号的类型关联,从而便于对本机操作系统数据结构进行基于源代码的访问。内核符号名称存储在与 D 变量和函数标识符不同的名称空间中,因此您无需担心这些名称会与 D 变量发生冲突。

至此,您已完成了 DTrace 旋风之旅,并了解了许多对于生成更大、更复杂的 D 程序所必需的基本 DTrace 生成块。在以下章节中,将描述用于 D 程序的整套规则,并演示 DTrace 如何使复杂的系统性能度量和功能分析变得更简单。随后,您将会了解到如何使用 DTrace 将用户应用程序行为与系统行为相连,从而使您可以分析整个软件栈。

这才刚刚开始!