Solaris 动态跟踪指南

结构指针

在 C 和 D 中,通过指针引用结构很常见。您可以使用运算符 -> 来通过指针访问结构成员。如果 struct s 包含成员 m,并且您具有指向名为 sp 的结构的指针(即 spstruct s * 类型的变量),则可以使用 * 运算符,首先取消对 sp 指针的引用,以便访问该成员:

struct s *sp;

(*sp).m

或者,可将 -> 运算符用作该表示法的简写。如果 sp 为指向结构的指针,则以下两个 D 代码段的含义完全相同。

(*sp).m				sp->m

DTrace 提供了多个属于结构指针的内置变量,包括 curpsinfocurlwpsinfo。这些指针分别引用结构 psinfolwpsinfo,并且其内容提供了有关当前进程及轻量进程 (lightweight process, LWP)(与触发当前探测器的线程关联)状态的信息快照。Solaris LWP 是用户线程的内核表示,Solaris 线程和 POSIX 线程接口是基于该进程建立的。为方便起见,DTrace 采用与 /proc 文件系统文件 /proc/pid /psinfo/proc/pid/lwps/ lwpid/lwpsinfo 相同的格式导出此信息。/proc 结构可供观察工具和调试工具(如 ps(1)pgrep(1)truss(1))使用,系统头文件 <sys/procfs.h> 中定义了该结构,proc(4) 手册页中也有其说明。下面列出了几个使用 curpsinfo 的示例表达式,及其类型和含义:

curpsinfo->pr_pid

pid_t

当前进程 ID 

curpsinfo->pr_fname

char []

可执行文件名 

curpsinfo->pr_psargs

char []

初始命令行参数 

您应在随后通过检查 <sys/procfs.h> 头文件以及 proc(4) 中的相应说明,查看完整的结构定义。下例将通过匹配命令行参数,使用 pr_psargs 成员来确定所关注的进程。

在 C 程序中,经常使用结构来创建复杂的数据结构,因此,描述和引用来自 D 的结构的能力也可提供强大的功能,以便观察 Solaris 操作系统内核及其系统接口的内部工作方式。除使用前述的 curpsinfo 结构外,下例还通过观察 ksyms(7D) 驱动程序和 read(2) 请求之间的关系,检查某些内核结构。该驱动程序使用两个称作 uio(9S)iovec(9S) 的常见结构,来响应从字符设备文件 /dev/ksyms 读取的请求。

uio(9S) 手册页介绍了可通过名称 struct uio 或类型别名 uio_t 访问的 uio 结构。该结构用于说明与在内核和用户进程之间复制数据有关的 I/O 请求。uio 又包含由一个或多个 iovec(9S) 结构组成的数组,如果使用 readv(2)writev(2) 系统调用请求多个块,则每个结构都说明一部分请求的 I/O。在 struct uio 上运行的内核设备驱动程序接口 (device driver interface, DDI) 例程中有一个是函数 uiomove(9F),该函数是内核驱动程序用于响应用户进程 read(2) 请求并将数据复制回用户进程的函数系列之一。

ksyms 驱动程序用于管理名为 /dev/ksyms 的字符设备文件,该文件似乎为包含内核符号表相关信息的 ELF 文件,但实际上是驱动程序使用当前装入内核的模块集创建的一种假象。该驱动程序使用 uiomove(9F) 例程来响应 read(2) 请求。下例说明,/dev/ksymsread(2) 的参数和调用将调用通过驱动程序与 uiomove(9F) 匹配,以将结果复制到 read(2) 中指定位置的用户地址空间。

我们可以使用 strings(1) 实用程序和 -a 选项,强制从 /dev/ksyms 进行串读取。请尝试在 shell 中运行 strings -a /dev/ksyms,然后查看生成的输出结果。在编辑器中,键入示例脚本的第一条子句,然后将其保存在名为 ksyms.d 的文件中:

syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
}

第一条子句使用表达式 curpsinfo->pr_psargs 来访问和匹配 strings(1) 命令的命令行参数,以便脚本在跟踪参数之前选择正确的 read(2) 请求。请注意,通过使用运算符 ==(左侧参数为 char 类型的数组,右侧参数为字符串),D 编译器将指示左侧参数应提升为字符串,并执行字符串比较。在某个 shell 中键入并执行命令 dtrace -q -s ksyms.d,然后在另 一个 shell 中键入命令 strings -a /dev/ksyms。执行 strings(1) 时,将会显示与以下示例类似的 DTrace 输出:


# dtrace -q -s ksyms.d
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
...
^C
#

此示例可使用常用的 D 编程方法进行扩展,以便跟踪该初始 read(2) 请求的线程深入内核。进入 syscall::read:entry 中的内核后,接下来的脚本将设置线程局部标志变量,用于指示此线程为关注线程,并在 syscall::read:return 中清除该标志。设置该标志后,可在其他探测器上将其用作谓词,以检测 uiomove(9F) 等内核函数。DTrace 函数边界跟踪 (function boundary tracing,fbt) 提供器发布了用于进入和返回该内核中定义的函数(包括 DDI 中的那些函数)的探测器。请键入下列使用 fbt 提供器检测 uiomove(9F) 的源代码,然后再次将其保存到文件 ksyms.d 中:


示例 7–2 ksyms.d:跟踪 read(2) 和 uiomove(9F) 关系

/*
 * When our strings(1) invocation starts a read(2), set a watched flag on
 * the current thread.  When the read(2) finishes, clear the watched flag.
 */
syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
	self->watched = 1;
}

syscall::read:return
/self->watched/
{
	self->watched = 0;
}

/*
 * Instrument uiomove(9F).  The prototype for this function is as follows:
 * int uiomove(caddr_t addr, size_t nbytes, enum uio_rw rwflag, uio_t *uio);
 */
fbt::uiomove:entry
/self->watched/
{
	this->iov = args[3]->uio_iov;

	printf("uiomove %u bytes to %p in pid %d\n",
	    this->iov->iov_len, this->iov->iov_base, pid);
}

该示例的最后一条子句使用线程本地变量 self->watched,来确定所关注的内核线程进入 DDI 例程 uiomove(9F) 的时间。在此之后,脚本使用内置的 args 数组访问 uiomove() 的第四个参数 (args[3]),该参数是一个指向代表该请求的 struct uio 的指针。D 编译器会自动将 args 数组的每个成员,与受检测的内核例程的 C 函数原型所对应的类型进行关联。uio_iov 成员包含一个指向该请求的 struct iovec 的指针。为供子句中的子句局部变量 this->iov 使用,保存了该指针副本。在最后一条语句中,脚本取消了对 this->iov 的引用,以访问 iovec 成员 iov_leniov_base,二者分别表示 uiomove(9F) 的长度(以字节为单位)以及目标基本地址。这些值应与驱动程序中发出的 read(2) 系统调用的输入参数匹配。请转到您的 shell 并运行 dtrace -q -s ksyms.d,然后在另一个 shell 中再次输入命令 strings -a /dev/ksyms。显示的输出将与以下示例类似:


# dtrace -q -s ksyms.d
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
...
^C
#

在您的输出中,地址和进程 ID 将有所不同,但您应看到 read(2) 的输入参数与通过 ksyms 驱动程序传递至 uiomove(9F) 的参数匹配。