Solaris 动态跟踪指南

第 3 章 变量

D 提供了两种基本类型的变量供在跟踪程序中使用:标量变量和关联数组。在第一章的示例中简单介绍了这些变量的用法。本章将详细介绍 D 变量的规则以及如何将变量与不同的作用域关联。在第 9 章中将讨论称为聚合的特殊类型的数组变量。

标量变量

标量变量用于表示各个大小固定的数据对象(如整数和指针)。标量变量也可用于表示由一个或多个原始类型或复合类型组成的大小固定的对象。D 提供了用于创建对象数组和复合结构的功能。DTrace 还通过允许字符串扩大到预定义的最大长度,将字符串表示为大小固定的标量。第 6 章中将进一步讨论如何在 D 程序中控制字符串的长度。

在 D 程序中首次对先前未定义的标识符赋值时,会自动创建标量变量。例如,要创建一个名为 xint 类型的标量变量,只需在任何探测子句中为该变量指定一个 int 类型的值:

BEGIN
{
	x = 123;
}

按照此方式创建的标量变量为全局变量:其名称和数据存储位置只需定义一次,并且在 D 程序的所有子句中都可见。每次引用标识符 x 时,就是在引用与此变量关联的单个存储位置。

与 ANSI-C 不同,D 编程语言不需要显式声明变量。如果确实要声明一个全局变量,以在使用该变量之前为其显式指定名称和类型,则可以在程序中探测子句外部进行声明,如下例所示。在大多数 D 程序中无需显式声明变量,但如果要谨慎控制变量类型,或要在程序的开头使用一组声明和注释来说明该程序的变量及其含意,显式声明变量会很有用。

int x; /* declare an integer x for later use */

BEGIN
{
	x = 123;
	...
}

与 ANSI-C 声明不同,D 变量声明不能指定初始值。必须使用 BEGIN 探测子句指定所有初始值。在首次引用变量之前,DTrace 将使用零填充所有全局变量存储空间。

D 语言定义对 D 变量的大小和数量不设限制,但是 DTrace 实现和系统中可用内存会定义一些限制。D 编译器在编译程序时将强制执行可应用的任何限制。在第 16 章中可了解有关如何调整与程序限制相关的选项的更多信息。

关联数组

关联数组用于表示数据元素的集合,可通过指定一个称为的名称来检索这些数据元素。D 关联数组的键由称为元组的标量表达式值的列表构成。您可以将数组元组本身视为函数的假想参数列表,在引用该数组时系统将调用该函数来检索相应的数组值。每个 D 关联数组都有一个固定的键签名,该签名由固定数量的元组元素组成,其中每个元素具有给定的固定类型。可以在 D 程序为每个数组定义不同的键签名。

关联数组与大小固定的常规数组不同,因为关联数组对元素的数量没有预定义的限制。关联数组的元素可以通过任何元组建立索引,而不是仅使用整数作为键,这些元素并不存储在预先分配的连续存储位置中。在 C、C++ 或 JavaTM 语言程序中要使用散列表或其他简单字典数据结构的情况下,使用关联数组会很有用。使用关联数组可以为 D 程序中捕获的事件和状态创建动态历史记录,使用该历史记录可创建更复杂的控制流。

要定义关联数组,请编写以下形式的赋值表达式:

name [ key ] = expression ;

其中,name 是任何有效的 D 标识符,key 是逗号分隔的一个或多个表达式的列表。例如,以下语句使用键签名 [ int, string ] 定义关联数组 a,并将整数值 456 存储在由元组 [ 123, "hello" ] 命名的位置中:

a[123, "hello"] = 456;

对于给定数组中的所有元素,数组中包含的每个对象的类型也是固定的。由于已对 a 指定了整数 456,所以该数组中存储的每个后续值也将为 int 类型。可以使用第 2 章中定义的任何赋值运算符,根据为每个运算符定义的操作数规则来修改关联数组元素。如果尝试进行不兼容的赋值,D 编译器将产生相应的错误消息。您可以将对标量变量使用的任何类型用于关联数组键或值。但不能将关联数组作为键或值嵌套在另一个关联数组中。

您可以使用与数组键签名兼容的任何元组引用关联数组。元组兼容性规则与函数调用和变量赋值的规则相似:元组的长度必须相同,且实际参数列表中的每种类型必须与正式键签名中的相应类型兼容。例如,如果按照如下所示定义关联数组 x

x[123ull] = 0;

则键签名为 unsigned long long 类型,值为 int 类型。也可使用表达式 x['a'] 引用该数组,因为根据类型转换中说明的算术转换规则,由 int 类型的字符常量 'a' 组成且长度为 1 的元组与键签名 unsigned long long 兼容。

如果需要在使用 D 关联数组前对其进行显式声明,可以在程序源代码中探测子句外部创建数组名称和键签名的声明:

int x[unsigned long long, char];

BEGIN
{
	x[123ull, 'a'] = 456;
}

定义关联数组后,将允许引用兼容键签名的任何元组,即使先前未对要引用的元组赋值。访问未赋值的关联数组元素定义为返回使用零填充的对象。此定义的后果是,在对关联数组元素赋非零值之前,将不会为该元素分配基础存储空间。相反,对关联数组元素赋值零将会导致 DTrace 解除分配基础存储空间。此行为很重要,因为可对关联数组元素分配的外部动态变量空间是有限的。如果在尝试分配时该空间已用完,分配将会失败,并生成一条错误消息,指示将进行动态变量删除。不再使用的关联数组元素将赋值零。有关消除动态变量删除的其他方法,请参见第 16 章

线程局部变量

利用 Dtrace,可以声明只作用于每个操作系统线程的局部变量存储空间,这与本章前面介绍的全局变量空间相对。在要启用探测器并使用一些标记或其他数据来标记将触发探测器的每个线程的情况下,可使用线程局部变量。在 D 中创建解决此问题的程序很方便,因为线程局部变量在 D 代码中共享一个公用名称,但引用与各个线程关联的独立数据存储空间。通过将 -> 运算符应用于特殊标识符 self 可引用线程局部变量:

syscall::read:entry
{
	self->read = 1;
}

此 D 代码段示例在 read(2) 系统调用中启用探测器,并将名为 read 的线程局部变量与触发该探测器的每个线程关联。与全局变量类似,线程局部变量在首次赋值时自动创建,并采用该赋值语句右边使用的类型(本例中为 int)。

每次在 D 程序中引用 self->read 变量时,引用的数据对象是与触发相应的 DTrace 探测器时执行的操作系统线程关联的对象。可将线程局部变量视为关联数组,其隐式索引是系统中线程标识组成的元组。线程的标识在系统的生命周期中是唯一的:如果该线程退出,并使用相同的操作系统数据结构创建一个新线程,则此新线程不会重用同一个 DTrace 线程局部存储标识。

定义线程局部变量后,即使先前未将要引用的变量指定给该特定线程,也可以在系统中的任何线程中引用该局部变量。如果尚未指定线程局部变量的线程副本,则该副本的数据存储空间将定义为使用零填充。与关联数组元素一样,在对线程局部变量指定非零值之前,不会为该变量分配基础存储空间。另外还与关联数组元素一样,为线程局部变量赋值零将会导致 DTrace 解除分配基础存储空间。不再使用的线程局部变量将赋值零。有关微调动态变量空间(从该空间分配线程局部变量)的其他方法,请参见第 16 章

可以在 D 程序中定义任何类型的线程局部变量,包括关联数组。以下是一些线程局部变量定义的示例:

self->x = 123;              /* integer value */
self->s = "hello";	          /* string value */
self->a[123, 'a'] = 456;    /* associative array */

与任何 D 变量一样,在使用线程局部变量之前,无需对其进行显式声明。如果确实要创建声明,可以通过在程序子句外部在声明之前加上关键字 self 来进行声明:

self int x;    /* declare int x as a thread-local variable */

syscall::read:entry
{
	self->x = 123;
}

线程局部变量保存在与全局变量不同的名称空间中,因此可以重用这些名称。请记住,如果在程序中过载名称,xself->x 是不同的变量。以下示例说明了如何使用线程局部变量。在文本编辑器中,键入以下程序并将其保存在名为 rtime.d 的文件中:


示例 3–1 rtime.d:计算 read(2) 中花费的时间

syscall::read:entry
{
	self->t = timestamp;
}

syscall::read:return
/self->t != 0/
{
	printf("%d/%d spent %d nsecs in read(2)\n",
	    pid, tid, timestamp - self->t);
	
	/*
	 * We're done with this thread-local variable; assign zero to it to
	 * allow the DTrace runtime to reclaim the underlying storage.
	 */
	self->t = 0;
}

现在进入 shell 并开始运行程序。等待几秒钟,您将会看到一些输出。如果没有显示输出,请尝试运行一些命令。


# dtrace -q -s rtime.d
100480/1 spent 11898 nsecs in read(2)
100441/1 spent 6742 nsecs in read(2)
100480/1 spent 4619 nsecs in read(2)
100452/1 spent 19560 nsecs in read(2)
100452/1 spent 3648 nsecs in read(2)
100441/1 spent 6645 nsecs in read(2)
100452/1 spent 5168 nsecs in read(2)
100452/1 spent 20329 nsecs in read(2)
100452/1 spent 3596 nsecs in read(2)
...
^C
#

rtime.d 使用名为 t 的线程局部变量捕获任何线程进入 read(2) 的时间标记。然后在返回子句中,程序通过从当前时间标记中减去 self->t,来列显 read(2) 中所花费的时间。内置 D 变量 pidtid 报告执行 read(2) 的线程的进程 ID 和线程 ID。因为报告此信息后将不再需要 self->t,随后将会对其赋值 0,使 DTrace 可以在当前线程中重用与 t 关联的基础存储空间。

通常,在没有执行任何操作的情况下,也可以看到多行输出,这是因为即使您未执行任何操作,服务器进程和守护进程始终都在后台执行 read(2)。尝试将 rtime.d 的第二条子句更改为使用 execname 变量,以列显执行 read(2) 的进程的名称,从而了解更多信息:

printf("%s/%d spent %d nsecs in read(2)\n",
    execname, tid, timestamp - self->t);

如果发现特别关注的进程,添加一个谓词以了解有关其 read(2) 行为的更多信息:

syscall::read:entry
/execname == "Xsun"/
{
	self->t = timestamp;
}

子句局部变量

也可以定义每个 D 程序子句将重用其存储空间的 D 变量。子句局部变量与 C、C++ 或 Java 语言程序中的自动变量类似,这些变量在每次调用函数期间处于活动状态。与所有 D 程序变量一样,子句局部变量会在首次赋值时自动创建。通过将 -> 运算符应用于特殊标识符 this 可引用这些变量并对这些变量赋值:

BEGIN
{
	this->secs = timestamp / 1000000000;
	...
}

如果要在使用子句局部变量前对其进行显式声明,可使用 this 关键字进行声明:

this int x;   /* an integer clause-local variable */
this char c;  /* a character clause-local variable */

BEGIN
{
	this->x = 123;
	this->c = 'D';
}

子句局部变量仅在给定探测子句的生命周期中处于活动状态。在 DTrace 执行与给定探测器的子句关联的操作后,将会回收所有子句局部变量的存储空间,并重用于下一条子句。因此,子句局部变量是唯一不使用零进行初始填充的 D 变量。请注意,如果程序的单个探测器包含多条子句,在执行这些子句时,所有子句局部变量将会保持不变,如下例所示:


示例 3–2 clause.d:子句局部变量

int me;			/* an integer global variable */
this int foo;		/* an integer clause-local variable */

tick-1sec
{
	/*
	 * Set foo to be 10 if and only if this is the first clause executed.
	 */
	this->foo = (me % 3 == 0) ? 10 : this->foo;
	printf("Clause 1 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

tick-1sec
{
	/*
	 * Set foo to be 20 if and only if this is the first clause executed. 
	 */
	this->foo = (me % 3 == 0) ? 20 : this->foo;
	printf("Clause 2 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

tick-1sec
{
	/*
	 * Set foo to be 30 if and only if this is the first clause executed.
	 */
	this->foo = (me % 3 == 0) ? 30 : this->foo;
	printf("Clause 3 is number %d; foo is %d\n", me++ % 3, this->foo++);
}

因为子句始终按程序顺序执行,且子句局部变量在启用相同探测器的不同子句中具有持久性,所以运行上述程序将始终产生同样的输出:


# dtrace -q -s clause.d
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
Clause 1 is number 0; foo is 10
Clause 2 is number 1; foo is 11
Clause 3 is number 2; foo is 12
^C

虽然子句局部变量在启用相同探测器的各条子句中具有持久性,但在为给定探测器执行的第一条子句中,未定义这些变量的值。在使用每条子句局部变量前,请务必为其指定相应的值,否则程序可能产生意外结果。

可使用任何标量变量类型定义子句局部变量,但不可以使用子句局部作用域定义关联数组。子句局部变量的作用域仅适用于相应的变量数据,而不适用于为变量定义的名称和类型标识。一旦定义子句局部变量,就可以在任何后续 D 程序子句中使用该名称和类型签名。存储位置在不同子句中不会一直保持不变。

可以使用子句局部变量积累计算的中间结果,或作为其他变量的临时副本。访问子句局部变量要比访问关联数组快得多。因此,如果需要在同一个 D 程序子句中多次引用关联数组值,首先将该值复制到一条子句局部变量中,然后重复引用该局部变量,将获得更高的效率。

内置变量

下表提供了 D 内置变量的完整列表。所有这些变量都是标量全局变量;D 语言中当前未定义线程局部变量、子句局部变量或内置关联数组。

表 3–1 DTrace 内置变量

类型和名称 

说明 

int64_t arg0, ..., arg9

探测器的前 10 个输入参数表示为原始的 64 位整数。如果传递到当前探测器的参数少于 10 个,则其余变量将返回 0。 

args[]

为当前探测器键入的参数(如果存在)。可使用整数索引访问 args[] 数组,但将每个元素定义为与给定探测器参数对应的类型。例如,如果通过 read(2) 系统调用探测器引用 args[],则 args[0]int 类型,args[1]void * 类型,args[2]size_t 类型。

uintptr_t caller

进入当前探测器之前的当前线程的程序计数器位置。 

chipid_t chip

当前物理芯片的 CPU 芯片标识符。有关更多信息,请参见第 26 章

processorid_t cpu

当前 CPU 的 CPU 标识符。有关更多信息,请参见第 26 章

cpuinfo_t *curcpu

当前 CPU 的 CPU 信息。有关更多信息,请参见第 26 章

lwpsinfo_t *curlwpsinfo

与当前线程关联的轻量进程 (lightweight process, LWP) 的 LWP 状态。此结构将在 proc(4) 手册页中详细说明。

psinfo_t *curpsinfo

与当前线程关联的进程的进程状态。此结构将在 proc(4) 手册页中详细说明。

kthread_t *curthread

当前线程 (kthread_t) 的操作系统内核的内部数据结构的地址。kthread_t<sys/thread.h> 中定义。有关此变量和其他操作系统数据结构的更多信息,请参阅 Solaris Internals

string cwd

与当前线程关联的进程的当前工作目录名称。 

uint_t epid

当前探测器的已启用的探测器 ID (enabled probe ID, EPID)。该整数唯一标识使用特定谓词和操作集启用的特定探测器。 

int errno

此线程最后一次执行的系统调用返回的错误值。 

string execname

传递到 exec(2) 以执行当前进程的名称。

gid_t gid

当前进程的实际组 ID。 

uint_t id

当前探测器的探测器 ID。此 ID 是当前探测器的系统范围内的唯一标识符,由 DTrace 发布,并列在 dtrace -l 的输出中。

uint_t ipl

触发探测器时当前 CPU 的中断优先级 (interrupt priority level, IPL)。有关 Solaris 操作系统内核中的中断级别和中断处理的更多信息,请参阅 Solaris Internals

lgrp_id_t lgrp

当前 CPU 所属的延迟组的延迟组 ID。有关更多信息,请参见第 26 章

pid_t pid

当前进程的进程 ID。 

pid_t ppid

当前进程的父进程 ID。 

string probefunc

当前探测器说明的函数名称部分。 

string probemod

当前探测器说明的模块名称部分。 

string probename

当前探测器说明的名称部分。 

string probeprov

当前探测器说明的提供器名称部分。 

psetid_t pset

包含当前 CPU 的处理器集的处理器集 ID。有关更多信息,请参见第 26 章

string root

与当前线程关联的进程的根目录名称。 

uint_t stackdepth

触发探测器时当前线程的栈帧深度。 

id_t tid

当前线程的线程 ID。对于与用户进程关联的线程,该值等于调用 pthread_self(3C) 的结果。

uint64_t timestamp

纳秒时间标记计数器的当前值。此计数器从过去的任意时间点递增,仅用于相对计算。 

uid_t uid

当前进程的实际用户 ID。 

uint64_t uregs[]

触发探测器时当前线程的已保存的用户模式寄存器值。uregs[] 数组的用法将在第第 33 章中讨论。

uint64_t vtimestamp

纳秒时间标记的当前值,实际是当前线程在 CPU 中已运行的时间减去 DTrace 谓词和操作中所花费时间而得到的值。此计数器从过去的任意时间点递增,仅用于相对时间计算。 

uint64_t walltimestamp

自 1970 年 1 月 1 日 00:00 世界标准时间以来的当前纳秒数。 

D 语言中的内置函数(如 trace())将在第 10 章中讨论。

外部变量

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

int kmem_flags;

要在 D 程序中访问此变量的值,请使用 D 表示法:

`kmem_flags

DTrace 将每个内核符号与在相应操作系统 C 代码中用于该符号的类型关联,从而使基于源代码访问本机操作系统数据结构更方便。要使用外部操作系统变量,需要访问相应的操作系统源代码。

从 D 程序访问外部变量时,访问的是其他程序的内部实现详细信息(如操作系统内核或其设备驱动程序)。这些实现详细信息不能构成可靠的稳定接口。在下次升级软件的相应部分时,所编写的依赖于这些详细信息的任何 D 程序可能会停止工作。因此,外部变量通常由内核和设备驱动程序开发者以及服务人员使用,以便使用 DTrace 调试性能或功能问题。要了解 D 程序稳定性的更多信息,请参阅第 39 章

内核符号名称存储在与 D 变量和函数标识符不同的名称空间中,因此您无需担心这些名称会与 D 变量发生冲突。在变量前加上一个反引号时,D 编译器为了找到匹配的变量定义,会使用已装入模块的列表按顺序搜索已知的内核符号。由于 Solaris 内核支持使用独立的名称空间动态装入的模块,所以在活动的操作系统内核中可多次使用相同的变量名称。可以通过在符号名称中指定应访问其变量的内核模块的名称并加上反引号,来解决这些名称冲突。例如,每个可装入内核模块通常提供一个 _fini(9E) 函数,以便引用名为 foo 的内核模块提供的 _fini 函数的地址,可写为:

foo`_fini

根据操作数类型的一般规则,可将任何 D 运算符应用于外部变量,但那些修改值的运算符除外。启动 DTrace 后,D 编译器将装入对应于活动内核模块的变量名称集,因此不需要声明这些变量。不能将修改变量值的任何运算符应用于外部变量,例如 =+=。出于安全原因,DTrace 将会阻止损坏或破坏所观察软件的状态。