星期五 四月 13, 2007
星期四 三月 22, 2007
在上一篇blog中,我们了解了DTrace的内置变量、函数、操作等。DTrace还内建了一些宏变量(Macro Variable),在D程序中可以直接使用这些宏变量,以增强D程序的可移植性。
表1 - D宏变量
| 名称 | 说明 | 参考相关系统调用 | |
| $[0-9]+ | 宏参数 | ||
| $egid | 有效组ID | getegid(2) | |
| $euid | 有效用户ID | geteuid(2) | |
| $gid | 实际组ID | getgid(2) | |
| $pid | 进程ID | getpid(2) | |
| $pgid | 进程组ID | getpgid(2) | |
| $ppid | 父进程ID | getppid(2) | |
| $projid | 项目ID | getprojid(2) | |
| $sid | 会话ID | getsid(2) | |
| $target | 目标进程ID | ||
| $taskid | 任务ID | gettaskid(2) | |
| $uid | 实际用户ID | getuid(2) |
在上表中,除宏参数和$target外,其它宏变量都与当时触发探测器的进程相关联。
宏参数表示传递给D程序的参数,如果D程序 macro.d接受3个参数,那么$0对应macro.d即D程序名,$1对应于第1个参数,$2对应于第2个参数,以此类推。如果要传递字符串给D程序,则相应的宏参数前面要再加上一个美元$符号。比如macro.d中,如果第3个参数是字符串,那么在D程序中应该使用$$3来引用。
$target被Dtrace替换为目标的进程号,如果是使用-p参数指定,则$target就是该进程号,如果是-c,则target对应-c后面的命令运行时的进程号。为便于大家理解,我们编写一个简单的D程序,只打印target的信息。
target.d
再编写一个测试用的shell脚本,此脚本只打印自己的进程号
pid.sh
然后我们执行以下操作
# echo $$
710
# ./target.d -p $$
target=710
# ./target.d -c ./pid.sh
mypid=766
target=766
怎么样,明白$target的含义了吧。
DTrace提供了可调整的选项,选项通过#pragma D option指定,有的选项也可以在命令行指定。
表2 - DTrace选项
| 选项名 | 命令行开关 | 值 | 描述 |
| aggrate | 时间或者频率(无后缀) | 聚合读取的频率 | |
| aggsize | 大小 | 聚合缓冲区的大小 | |
| bufresize | auto或者manual | 缓冲区调整大小的策略 | |
| bufsize | -b | 大小 | 主缓冲区大小 |
| cleanrate | 时间 | 清除的频率 | |
| cpu | -c | CPU标号 | 指定在该CPU上启用探测器跟踪 |
| defaultargs | 允许引用未指定的宏参数 | ||
| destructive | -w | 允许破坏性操作 | |
| dynvarsize | 动态变量空间大小 | ||
| flowindent | -F | 在进入函数时缩进显示,并加前缀->,退出函数时取消缩进,并加前缀<- | |
| grabanon | -a | 声明匿名跟踪状态 | |
| jstackframe | 数字 | 缺省的jstack()栈帧的数量 | |
| jstackstrsize | 数字 | jstack()缺省字符串大小 | |
| nspec | 数字 | 推理缓冲区的个数 | |
| quiet | -q | 仅输出显示跟踪的数据(比如printf) | |
| specsize | 大小 | 推理缓冲区的大小 | |
| strsize | 大小 | 字符串大小 | |
| stackframes | 数字 | 栈帧数 | |
| stackindent | 数字 | 当缩进stack()和ustack()是的空 | |
| statusrate | 时间 | 状态检查的频率 | |
| switchrate | 时间 | 缓冲区切换的频率 | |
| ustackframes | 数字 | 用户栈帧数 |
下面我们来了解一下DTrace中缓冲区及其管理(Data buffering and management)。
缓冲区及其管理是DTrace架构为其消费者提供的重要服务。DTrace操作有很多都是与数据记录相关的,这些数据是记录在DTrace的缓冲区中的。每次DTrace调用都会使用到“主缓冲区(Principal Buffer)”,主缓冲区是基于每个CPU分配的。对于缓冲区的管理有以下策略。
switch策略
缺省情况下,主缓冲区采用switch策略。在此策略下,每个CPU的缓冲区成对分配:一个处于活动状态,另一个处于非活动状态。当DTrace使用者试图访问缓冲区时,内核会切换(switch)活动缓冲区和非活动缓冲区,切换方式会保证跟踪的数据不会丢失。切换完成后,新的非活动缓冲区将复制给DTrace使用者。切换的速率可以通过switchrate选项控制,如果不带时间后缀,则缺省是每秒的次数。可以使用bufsize来调整主缓冲区的大小。
fill策略
在此策略下,当任何一个CPU的缓冲区已经填充满时,Dtrace将停止跟踪,并处理所有缓冲区。 要使用此策略,需要将bufpolicy设置为fill,可以使用命令行-x bufpolicy=fill或者编译指令#pragma D option bufpolicy=fill
ring策略
在此策略下,DTrace将主缓冲区作为一个环形缓冲区对待,即当缓冲区填满时,数据会重新从缓冲区开始记录。Dtrace只会在程序终止时才会输出信息。此策略指定方式,命令行-x bufpolicy=ring,编译指令#pragma D option bufpolicy=ring
其它缓冲区
除了上面的缓冲区外,还有:聚合缓冲区(aggregate buffer)以及一个或者多个推理缓冲区(Speculative buffer)。聚合缓冲区是聚合函数会用到的缓冲区,而推理缓冲区则是推理跟踪会用到的缓冲区。
聚合
如果需要调查与性能相关的系统问题,就可以用到Dtrace提供的聚合操作。聚合操作是针对聚合函数(Aggregating Functions)而言的。聚合函数具有以下属性:
f( f(X0) U f(X1) U ... U f(Xn) ) = f ( X0 U X1 U ... U Xn )
换句话讲,就是对整个数据集合的子集应用聚合函数,然后再对结果应用该聚合函数,得到的最终结果与对整个数据集合本身应用该聚合函数相同。比如求给定数据集合之和的SUM函数,就是一个聚合函数。
DTrace中的聚合函数见下表:
表3 - DTrace聚合函数
| 函数名 | 参数 | 结果 |
| count | 无 | 调用次数 |
| sum | 标量表达式 | 所指定表达式的总和 |
| avg | 标量表达式 | 所指定表达式的算术平均值 |
| min | 标量表达式 | 所指定表达式的最小值 |
| max | 标量表达式 | 所指定表达式的最大值 |
| lquantize | 标量表达式,下限,上限,步长值 | 所指定表达式的值的线性频率分布 |
| quantize | 标量表达式 | 所指定表达式的值的二次方幂频率分布 |
DTrace将聚合函数的结果存储在称为聚合(Aggregation)的特殊对象中。其语法为:
@name[keys]=aggfunc(args);
name是聚合的名称,可以省略,keys是索引,可以是有逗号分隔的表达式,aggfunc是上表提到的函数,args是聚合函数的参数。聚合与关联数组的区别是其名字是以@作为前缀的,@name与name在不同的名称空间。
比如我们想查看5秒钟内系统调用的次数
# dtrace -n 'syscall:::entry{@counts["syscall numbers"]=count();}tick-5sec{exit(0);}'
dtrace: description 'syscall:::entry' matched 232 probes
CPU ID FUNCTION:NAME
0 49049 :tick-5sec
syscall numbers 241
此例中聚合@count的key是字符串"syscall numbers"。
我们还想再进一步了解到底是什么程序调用的系统调用最多,可能这个程序就是导致系统性能下降的主谋
# dtrace -n 'syscall:::entry{@counts[execname]=count();}tick-5sec{exit(0);}'
dtrace: description 'syscall:::entry' matched 232 probes
CPU ID FUNCTION:NAME
0 49049 :tick-5sec
svc.configd 1
svc.startd 1
Xorg 4
nmbd 4
sendmail 10
dtrace 229
在此例中@counts的key是D内置变量execname。
在进一步细化,看看什么系统调用最多
# dtrace -n 'syscall:::entry{@counts[execname,probefunc]=count();}tick-5sec{exit(0);}'
dtrace: description 'syscall:::entry' matched 232 probes
CPU ID FUNCTION:NAME
0 49049 :tick-5sec
automountd gtime 1
dtrace mmap 1
dtrace schedctl 1
fmd lwp_park 1
in.routed pollsys 1
inetd lwp_park 1
sendmail pollsys 1
automountd doorfs 2
sendmail lwp_sigmask 2
dtrace sysconfig 3
sendmail pset 3
dtrace sigaction 4
sendmail gtime 4
dtrace lwp_park 5
dtrace brk 8
dtrace p_online 32
dtrace ioctl 177
在此例中@counts的key是execname,probefunc。
使用lquantize,我们了解需要调查的表达式的分布情况。比如,我们想知道系统调用write打开的文件描述符(file descriptor)的线性分布情况。
# dtrace -n 'syscall::write:entry{@fds[execname]=lquantize(arg0,0,100,1)}'
dtrace: description 'syscall::write:entry' matched 1 probe
^C
dtrace
value ------------- Distribution ------------- count
0 | 0
1 |@@@@@@@@@@@@@@@@@@@@ 1
2 | 0
sshd
value ------------- Distribution ------------- count
3 | 0
4 |@@@@@@@@@@@@@@@@@@@@ 1
5 | 0
6 | 0
7 | 0
8 |@@@@@@@@@@@@@@@@@@@@ 1
9 | 0
在上例中,我们可以看到,在该时间内,sshd进程对文件描述符4操作了1次,对文件描述符8操作了1次。虽然不具有实际意义,但可以帮助我们理解lquantize的作用。
如果要聚合的表达式的值非常大,使用lquantize可能会输出太多信息,这种情况下可以使用quantize来聚合。
下面是一个统计执行程序系统调用的时间分布的D脚本: time.d
#!/usr/sbin/dtrace -s
syscall:::entry
{
self->ts=timestamp;
}
syscall:::return
/self->ts/
{
@time[execname]=quantize(timestamp-self->ts);
}
执行一段时间,按Ctrl+C中断。限于篇幅,下面只列出部分信息。
# ./time.d
dtrace: script './time.d' matched 462 probes
^C
sendmail
value ------------- Distribution ------------- count
1024 | 0
2048 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 7
4096 |@@@@ 1
8192 |@@@@ 1
16384 | 0
sshd
value ------------- Distribution ------------- count
1024 | 0
2048 |@@@@@@@@@@@@@@@@@@@ 7
4096 |@@@@@ 2
8192 |@@@@@ 2
16384 |@@@@@ 2
32768 | 0
65536 |@@@@@ 2
131072 | 0
以sendmail程序为例:
系统调用执行时间(从entry到return)在大于等于2048纳秒并小于4096纳秒区间共有7次,在大于等于4096纳秒小于8192纳秒区间共有1次,在大于等于8192纳秒小于16384纳秒区间共有1次。
在聚合一段时间后,可能需要对某个常数因子进行标准化(normalize),以更好的分析数据。如下例,我们按照执行的时间来标准化聚合数据,以得到每秒钟的系统调用数。
}
标准化不会修改原始数据。与标准化相对应的是“取消标准化(denormalize)"函数,此函数可以将聚合恢复到标准化之前的状态。
聚合的数据会随着时间的增加而增加,你可以定期使用clear()和trunc()函数进行清除。clear()与trunc()的区别是clear()只会清除聚合的值,而trunc()则会同时清除聚合的值和键(key)。
下面的例子每秒钟打印上一秒程序对系统调用的使用情况
# dtrace -n 'syscall:::entry{@counts[execname]=count();}tick-1sec{printa(@counts);trunc(@counts);}'
dtrace: description 'syscall:::entry' matched 232 probes
CPU ID FUNCTION:NAME
0 49050 :tick-1sec
syslogd 16
dtrace 91
0 49050 :tick-1sec
fmd 1
inetd 1
sendmail 11
sshd 40
dtrace 45
0 49050 :tick-1sec
sshd 8
dtrace 42
0 49050 :tick-1sec
sshd 8
dtrace 39
0 49050 :tick-1sec
nmbd 4
sshd 8
dtrace 39
0 49050 :tick-1sec
svc.configd 1
svc.startd 1
sshd 8
dtrace 40
0 49050 :tick-1sec
sshd 8
sendmail 10
dtrace 41
^C
在分析实际的性能问题时,建议使用聚合作为你的出发点。
推理跟踪(Speculative Tracing)
推理跟踪是DTrace提供的用于试探性地跟踪数据的工具,它可以在事后才来决定是将这些数据提交(commit())到跟踪缓冲区,还是放弃(discard())。
DTrace为推理跟踪提供了以下函数:
表4 - DTrace推理函数
| 函数名 | 参数 | 说明 |
| speculation | 无 | 返回一个新的推理缓冲区的标志符 |
| speculate | 推理缓冲区的标志符ID | 子句的其余部分会把数据存放到由ID指定的推理缓冲区里 |
| commit | 推理缓冲区的标志符ID | 提交与ID相关的推理缓冲区 |
| discard | 推理缓冲区的标志符ID | 放弃与ID |
推理缓冲区是一种有限的资源,如果无推理缓冲区可用,则speculation()返回0,表示无效的ID。
speculate()操作要放在所有需要跟踪的数据记录操作之前。speculate()不能放在数据记录之后,否则DTrace会编译出错。不能对聚合操作,破坏性操作和exit()进行推理跟踪。通常的做法是将speculation()的结果赋给线程局部变量,然后使用该变量作为其它探测器的谓词以及speculate()的参数。
当推理缓冲区被提交时,其数据将被复制到主缓冲区中。如果放弃推理缓冲区,其数据将被丢弃。
下面的示例einvalspec.d展示了推理跟踪的一种应用方式,用来显示特定的代码路径。当系统调用返回错误代码EINVAL时,我们就打印出其代码路径。
#pragma D option flowindet 表示当进入函数时,缩进显示,并加上前缀 ->,当退出函数时,取消缩进,并加上前缀<-
#pragma D option nspec=200 表示推理缓冲区的个数(如果不指定,缺省只有一个)
/* */ 之间的内容是注释,
discard(self->spec); /*不是我们关心的情况,丢弃推理缓冲区数据*/
你还可以将上面程序中的EINVAL改为你关心的其它错误代码(具体错误代码信息,请查阅intro(2)手册页)。
星期三 三月 21, 2007
通过上一次的介绍,相信大家对DTRACE已经有了一个初步的认识。上一次结束时专门留了一个例子,可能大家第一次看有很多不明白的地方,没有关系,随着我们对DTRACE更多的介绍,很快就会”云开雾散“了。
D语言作为一种编程语言,自然就有其语法、关键字、数据结构、运算符、函数等,我将一一介绍。
D语言中标志符名称与C语言类似,由字母、数字和下划线组成,其中第一个字符必须是字母或者下划线。D语言预留了一些关键字供DTRACE本身使用,关键字不能用做D变量的名称。D关键字列表参阅《Solaris动态跟踪指南》,这里只列出一些常用的。
表1 - 常用DTRACE关键字
| 关键字 | 描述 |
| inline | 编译期间将指定的D变量替换为预定义的值或者表达式,inline可以申明类型 |
| sizeof | 计算对象的大小 |
| self | 表示将D变量存放在线程(thread)的私有空间里 |
| this | 表示D变量的有效范围在this所在的子句内 |
D语言中定义了整数类型和浮点类型,以及用于表示ASCII字符串的string类型。整数类型随机器字长的不同而不同。机器字长可以用命令isainfo -b来查看。
表2 - D整数类型
| 类型名称 | 32位机器字长 | 64位机器字长 |
| char | 1个字节 | 1个字节 |
| short | 2个字节 | 2个字节 |
| int | 4个字节 | 4个字节 |
| long | 4个字节 | 8个字节 |
| longlong | 8个字节 | 8个字节 |
一点小知识,C语言中有ILP32和LP64两种数据模型,ILP32指的就是int/long/pointer(指针)是32位,LP64指的是long/pointer是64位。
对于整数类型,又分为带符号(signed)和无符号(unsigned)两种,因为是否带符号决定了对其最高位(most-significant)的解释。无符号整型通常是在相应的整型前面添加unsigned或者u限定符。
表3 - D整数类型别名
| 类型名称 | 说明 |
| int8_t / uint8_t | 1字节带符号整数 / 1字节无符号整数 |
| int16_t / uint16_t | 2字节带符号整数 / 2字节无符号整数 |
| int32_t / uint32_t | 4字节带符号整数 / 4字节无符号整数 |
| int64_t / uint64_t | 8字节带符号整数 / 8字节无符号整数 |
| intptr_t / uintptr_t | 大小等于指针的带符号整数 / 大小等于指针的无符号整数 |
D语言中也定义了转义序列如'\n'表示回车。
D语言中定义了算术运算符、关系运算符、逻辑运算符、按位运算符、赋值运算符、递增和递减运算符、条件表达式(即 ? : 运算符),由于这些运算符及其优先级与C语言基本相同,就不在这里占用篇幅了。D语言也支持”强制类型转换“,即把一种类型转换为另一种兼容类型,比如将指针转换为整数。
除了上面的数据类型,D语言还提供有数组(array)和关联数组(associative array),关联数组中有一种特殊类型叫做聚合(aggregation),在后面会看到。关联数组通过一个称为键(key)的名称来检索数据,用过Perl的朋友相信不会陌生。定义关联数组,只需作以下赋值操作即可:
name[key]=expression;
例如: people["sam.wan",30]=100
D语言中的变量是不需要预定义就可以直接使用的。但是在没有赋值之前,是不能出现在谓词中和赋值运算等号右侧。请看下面的3个例子:
例子1
# dtrace -n 'BEGIN{a=1;exit(0);}END{printf("a=%d\n",a);}'
dtrace: description 'BEGIN' matched 2 probes
CPU ID FUNCTION:NAME
0 1 :BEGIN
0 2 :END a=1
例子2
# dtrace -n 'BEGIN/a==0/{exit(0);}END{printf("a=%d\n",a);}'
dtrace: invalid probe specifier BEGIN/a==0/{exit(0);}END{printf("a=%d\n",a);}: in predicate: failed to resolve a: Unknown variable name
例子3
# dtrace -n 'BEGIN{a=a+1;exit(0);}END{printf("a=%d\n",a);}'
dtrace: invalid probe specifier BEGIN{a=a+1;exit(0);}END{printf("a=%d\n",a);}: in action list: a has not yet been declared or assigned
缺省情况下,D语言中定义的变量是全局范围的。在多线程环境中,全局变量是不安全的,因为可能多个线程都会进行访问,因此,D语言引入了线程局部变量标志符self。通过在一个变量前面添加self->修饰,可以将该变量存放在线程自己的局部空间中,这样不会受到其它线程的影响,这种方法对于现在越来越多的并发操作环境十分有利。线程局部变量与全局变量在不同的名称空间(name space)中,因此即使名字相同也不会冲突,比如self->aaa和aaa是两个不同的变量。
特别需要提醒注意的是,在使用完一个变量之后,要将该标量赋值为'0',这样DTRACE就会回收释放其所占用的内存空间。对用一个好的程序员来说,释放空间和分配空间同样重要。
D语言中还有一种特殊的变量叫“子句局部变量(Clause Local)”,通过在变量名前添加this->修饰符完成。子句局部变量的作用域只在其定义的子句内有效。D语言中的变量缺省情况下会被赋值为0,但是子句局部变量除外。
除用户定义的变量外,D语言本身提供了一些非常有用的内置变量,所有这些内置变量都是全局变量。
表4 - DTrace内置变量
| 类型和名称 | 说明 |
| int64_t arg0,...,arg9 | 探测器的前10个输入参数(64位整数)。如果当前探测器参数个数少于10,则未定义的参数值不确定 |
| args[] | 与arg0...arg9不同,args[]是有类型的,其类型对应与当前探测器的参数类型。 |
| uintptr_t caller | 进入当前探测器之前的当前线程的程序计数器(PC)位置 |
| chipid_t chip | 当前物理芯片的CPU芯片标志符 |
| processorid_t cpu | 当前CPU的编号 |
| cpuinfo_t *curcpu | 当前CPU的信息(具体结构后面会讲到) |
| lwpsinfo_t *curlwpsinfo | 与当前线程关联的轻量进程(LightWeight Process,LWP)的信息(具体结构见后) |
| psinfo_t *curpsinfo | 与当前线程关联的进程的信息 |
| kthread_t *curthread | 当前线程在内核中的数据结构(kthread_t)的地址,kthread_t的定义在<sys/thread.h>中。 |
| string cwd | 当前进程的工作路径(Current Working Directory) |
| uint_t epid | 当前探测器的已启用的探测器ID号。 |
| int errno | 当前线程最后一次执行的系统调用的返回错误值 |
| string execname | 当前进程的名称 |
| gid_t gid | 组ID |
| uint_t id | 当前探测器的唯一ID号,dtrace -l的第一列 |
| uint_t ipl | 触发探测器时当前CPU的中断优先级(Interrupt Priority Level,IPL)。 |
| lgrp_id_t lgrp | 当前CPU所属的延迟组(Latency Group)的ID |
| pid_t pid | 当前进程号 |
| pid_t ppid | 当前进程的父进程 |
| string probefunc | 当前探测器的函数名 |
| string probemod | 当前探测器的模块名 |
| string probename | 当前探测器的名字 |
| string probeprov | 当前探测器的提供器名 |
| psetid_t pset | 当前CPU所属的处理器集(Processor Set)的ID |
| string root | 当前进程的根目录名 |
| uint_t stackdepth | 当前线程的栈帧(Stack Frame)的深度。即其调用的函数的层次数。 |
| id_t tid | 当前线程的线程ID |
| uint64_t timestamp | 以纳秒(ns)为单位的时间计数器。此计数器从过去的任意点递增,仅用于相对计算中。 |
| uid_t uid | 当前进程的实际用户ID |
| uint64_t uregs[] | 当前线程的用户寄存器值 |
| uint64_t vtimestamp | 以纳秒(ns)为单位的时间计数器,实际是当前线程在CPU中已运行的时间减去DTrace谓词和操作所花费的时间。同timestamp一样,仅用于相对计算。 |
| uint64_t walltimestamp | 自1970年1月1日00:00世界标准时间以来的纳秒数。 |
Dtrace还可以使用反引号(backquote `)访问操作系统内核中定义的变量,但不能进行修改。
内核中定义的变量可以通过查看内核的变量符号表(Name Symbol)来找到。
#echo "::nm"|mdb -k|more
比如我们想通过DTrace来查看每秒钟freemem的值。freemem表示当前系统中可用的内存页数
#dtrace -qn 'tick-1sec{printf("%d pages of freemem\n",`freemem)}'
由于Solaris可用动态加载模块,各个模块可能有相同的变量名,为了避免冲突,可用使用模块名来区分,比如访问a模块的x变量:a`x
DTrace还提供操作和子例程。
如果DTrace子句为空,则会采用缺省操作。缺省操作即显示已启用的探测器的标志符。
数据记录操作
数据记录操作总会往指定的缓冲区中放入数据。
void trace(expression) 将expression的结果放到指定的缓冲区(在后面会提到)。
void tracemem(address,size_t nbytes),从address地址复制nbytes的内容到指定缓冲区。
void printf(string format,...) 格式化输出。具体格式可用参见printf(3C)手册页。
void printa(aggregation)/void printa(string format,aggregation) 显示及格式化聚合(在后面会提到)
void stack(void)/void stack(int nframes) 将指定长度的栈帧记录拷贝到指定的缓冲区。
void ustack(int nframes,int size)/void ustack(int nframes)/void ustack(void),同上,只是操作的是用户栈
破坏性操作
void stop(void) 停止触发当前探测器的进程
void raise(int signal) 将指定的信号signal发送至触发当前探测器的进程
void copyout(void *buf,uintptr_t addr,size_t nbytes) 从buf地址拷贝nbytes字节到当前进程的addr地址处。
void copyoutstr(string str,uintptr_t addr,size_t maxlen) 将字符串string拷贝到当前进程的addr地址处
void system(string program,..) 执行程序
内核破坏性操作(下面的操作将会影响整个系统的运行)
void breakpoint(void) 发生一个内核断点
void panic(void) 触发panic()操作(这个相信大家都再熟悉不过了)
void chill(int nanoseconds) DTrace执行nanoseconds时间的spin操作(循环),如果nanoseconds> 500milliseconds,则会失败。
特殊操作
推测性操作(Speculative Actions),有speculate(),commit(),discard(),在后面会提到。
void exit(int status) 立即停止DTrace跟踪。
子例程
与操作不同,子例程只会影响DTrace的内部状态。
void *alloca(size_t size) 分配size字节的临时空间,返回一个8字节对齐的指针。
string basename(char *str) 从str中去除前缀和目录名
void bcopy(void *src,void *dest,sizt_t size) 从src拷贝size字节到dest。
string cleanpath(char *str) 去除str中的/./和/../等
void *copyin(uintptr_t addr,size_t size) 从用户地址空间addr处拷贝size字节到Dtrace临时缓冲区中,并返回缓冲区地址。
string *copyinstr(uintptr_t addr) 从用户地址空间addr除拷贝已null结尾的ASCII字符串到Dtrace临时缓冲区,并返回缓冲区地址。
void copyinto(uintptr_t addr,size_t size,void *dest) 从用户地址空间addr处拷贝size字节到Dtrace临时缓冲区的dest处。
string dirname(char *str) 返回str的目录名
size_t msgdsize(mblk_t *mp) 返回mp指向的数据消息的字节数
size_t msgsize(mblk_t *mp) 返回mp消息字节数
int mutex_owned(kmutex_t *mutex) 如果当前线程拥有互斥锁mutex,则返回非零;否则返回0
kthread_t *mutex_owner(kmutex_t *mutex) 返回mutex互斥锁的属主的线程数据结构kthread_t的指针。如果没有属主或者该互斥锁是自旋锁(Spin Mutex),则返回null。
int mutex_type_adaptive(kmutex_t *mutex) 如果mutex是自适应互斥锁(MUTEX_ADAPTIVE类型),则返回非0,否则返回0。
int progenyof(pid_t pid) 如果触发当前探测器的进程是指定进程的子孙,则返回非0
int rand(void) 返回一个伪随机整数
int rw_iswriter(krwlock_t *rwlock) 如果指定的读写锁rwlock被一个写入者占有或者要求获得,则返回非0,否则返回0
int rw_write_held(krwlock_t *rwlock) 如果指定的读写锁当前被一个写入者占有,则返回非0,否则返回0
int speculation(void) 为speculate()操作预留一个推测性跟踪缓冲区,并返回这个缓冲区的标志符。
string strjoin(char *str1,char *str2) 串联str1和str2到临时空间,并返回其地址。
size_t strlen(string str) 返回指定字串的长度(不包括结尾的空字节null)
看了这么多操作和子例程,是不是有点受打击了?没有关系,慢慢来,先熟悉一下,在今后具体使用时,就会印象深刻。
关于前面的copyin/copyinstr/copyinto子例程再多作一点说明:
在Solaris(UNIX)系统中,用户程序是运行在用户地址空间里面,当用户程序执行系统调用比如open(2)时,才会进入到内核空间执行(我们通常称之为"陷入trap"
,为了访问用户地址空间的字符串,就必须将其拷贝到内核空间里面来,否则内核找不到相应的地址,就会报错。看下面的一个例子。
我们想知道是什么程序在调用open(2),以及打开什么文件。这里很自然我们会用到syscall提供器的open:entry探测器。此探测器的参数可用从open(2)的手册页查到(所有的syscall提供器提供的探测器都可用在相对应的系统调用手册中查到)
int open(const char *path, int oflag, /* mode_t mode */);
我们只关心第一个参数arg0,这是一个字符串指针(即将要打开的文件名)。对于字符串指针,可以使用"%s"进行格式化输出。
运行一下看看
# ./who_open_what.d
dtrace: failed to compile script ./who_open_what.d: line 5: printf( ) argument #4 is incompatible with conversion #3 prototype:
conversion: %s
prototype: char [] or string (or use stringof)
argument: int64_t
错误,为什么,因为传递给内核的是一个用户地址空间的指针,内核无法访问该地址,内核只看到一个指针,因此Dtrace认为格式化用的是"%s",但是传递的却是一个int64_t类型,不匹配。
正确的程序应该是:
再来看看
# ./who_open_what.d
nfsmapid[272] opened /etc/default/nfs
nfsmapid[272] opened /etc/resolv.conf
init[1] opened /etc/inittab
init[1] opened /etc/svc/volatile/init-next.state
init[1] opened /etc/svc/volatile/init-next.state
init[1] opened /etc/inittab
...
是不是很有趣。
更多有趣的还在后头,别走开哦 
星期二 三月 20, 2007
记得几年前看过一部美国大片叫《全民公敌(Enemy of the State)》,在里面,谋杀国会议员的主谋强沃特和他的属下,为了取回记录着其犯罪事实的磁碟片,用高科技的卫星监视,使主人公史密斯的行踪处于严密的监控中。当时就对美国高科技跟踪系统惊叹不已。当然作为一个普通公民,是不希望自己受到监视的。但是对于计算机系统,如果能够对系统的运行情况进行监视并了如指掌,进而发现其中的臭虫(bug),那将是一件令IT管理者和开发者兴奋的事。今天我要介绍的SolarisTM Dtrace就是这样一个好帮手!
我的第一篇Blog就提到了Dtrace,但是没有作更多的说明。今天我将对Dtrace作比较详细的介绍,一是作为自己学习Dtrace的一点心得,二是希望对还没有使用Dtrace的朋友们提供一点入门知识,更详细的信息请参阅第一篇Blog中提到的资源。为了与中文版的《Solaris动态跟踪指南》保持一致,下面的术语都采用书中的翻译。
DTRACE(全称Dynamic Tracing)是SolarisTM 10中引入的一种可以对核心(kernel)和应用程序(user application)进行动态跟踪并且对系统运行不构成任何危险的技术。下面是理解Dtrace的几个要点:
1. Dtrace的实现是紧密地结合到核心里的(intimately integrated),即Dtrace的源代码是分布到了Kernel的各个部分中。除了Dtrace的执行程序dtrace.c和头文件<sys/dtrace.h>,<sys/dtrace_impl.h>外,其它实现dtrace的代码遍布到Solaris Source tree的各个文件。具体请参见 Bryan Cantrill的Blog - "The Observation Deck"
2. Dtrace架构中一个很重要的组件是"探测器(Probe)",简单讲,探测器就是核心源代码中某一个特点的”点“。在普通的Solaris 10内核中,这样的”点“有4万多个,而且还可以随着模块的加载而增加。探测器在没有被”启用(enable)“时,对核心是没有任何影响的,这时的核心与没有dtrace功能的核心如Solaris 8/9是没有任何区别的。当探测器被启用后,Solaris会动态地往核心中为启用的探测器加入相应的指令来实现探测器被"触发(fire)"时的“操作(action)"。
3. Dtrace架构可以简单的理解为”Dtrace提供器(Provider)和Dtrace使用者(Consumer)”模式。如下图所示:

”提供器“提供了”探测器“,而”使用者“通过libdtrace(3LIB)库和相应的设备文件或者其它方式来使用”提供器“提供的”探测器"。如上图所示,除了我们下面将会介绍的/usr/sbin/dtrace命令外,Solaris 10系统中还有很多收集统计信息的工具比如intrstat(1M),plockstat(1M),lockstat(1M)等都是Dtrace使用者。使用plockstat -V -p <pid>,你就可以看到plockstat使用的dtrace命令。
4. Dtrace本身是安全的,即不会对内核的运行造成影响。Dtrace可以读取内核变量,却不能修改内核变量。但是Dtrace提供了”破坏性(destructive)"的操作比如panic(),如果你使用了这些动作,是会中断系统运行的。
在学习Dtrace的过程中,要切记上面的几点。
下面就重点介绍一下Dtrace中日常使用最频繁的一个Dtrace使用者/usr/sbin/dtrace命令。dtrace(1M)可以以命令行形式调用,也可以通过D-script调用。D-script是用Dtrace提供的D语言来编写的脚本程序。D语言类似于C和awk,但是没有程序控制如for,if等机制,也许是为了更好的控制系统的稳定性。
命令行调用的例子: dtrace -n 'syscall::open*:entry{trace(execname)}'
D-script例子:
#!/usr/sbin/dtrace -s syscall::open:entry, syscall::open64:entry { trace(execname); } |
不管是命令行方式还是脚本方式,都要指定至少一个探测器。每个探测器都是一个“四元组(4-tuple)",但是有的部分可以省略。探测器的具体格式如下:
Provider:Module:Function:Name
各部分的含义如下:
- Provider即提供器,发布此探测器的Dtrace提供器的名称。比如:syscall是所有系统调用的提供器,sysinfo是系统统计信息的提供器,proc是进程信息的提供器。不同系统不同版本的Solaris的提供器的数量不同。使用下面的命令可以查看系统中有多少个提供器.
#dtrace -l|grep -v "PROVIDER"|awk '{print $2}'|sort -u
- Module即模块,是此探测器对应于特定的程序位置时,其所在模块的名称。对于应用程序,模块名可以是动态链接库的名字,比如:libc,或者主程序a.out。有的探测器没有模块名。
- Function即函数,探测器所在函数的名称
- Name即名字,最后一个组成部分。
探测器的四元组名字如果某个部分为空,则表示匹配该字段的所有可能性,星号(*)也是通配符,表示匹配任意字符串。现在我们再来看上面的两个例子。第一个命令行例子表示启用syscall提供器中所有模块里面名字以open开头的函数的entry探测器;而第二个脚本例子表示匹配syscall提供器中所有模块里面名字是open或者open64函数的entry探测器,其中的逗号表示或者的关系。命令行方式调用时,如果不使用-l开关,则指定的探测器将被启用,对于脚本方式,-s后面即D-script程序的正文部分。
一个D程序的结构如下:
上面的伪代码(pseudo-code)描述了一个D程序的大致结构,其中除了探测器描述部分,其它的部分如谓词、操作都不是必须的。第0行指明D程序的解释器(interpreter),就是/usr/sbin/dtrace;第1行使用pragma关键字指定特定的D程序编译指令;从第2行起就是对相应的探测器的启用,并定义在指定的探测器被触发时应该执行的操作,操作以分号结尾。其中,在探测器描述和操作之间用 / / 符号隔开的部分称为"谓词(Predicate)"。前面已经提到,在D语言中,没有if语句和循环,只有通过谓词来进行判断,谓词是一系列的逻辑运算,如果计算结果是false(0),则忽略探测器的触发,当然更不会执行该探测器定义的任何操作;只有当谓词计算为true(非0)时,相应的操作才会被执行。D程序的执行是从上至下顺序执行的,花括号{}包围的部分是对应探测器被触发且谓词为真时的执行子句块,对于同一个探测器描述,可以指定多个执行子句块。
当你编辑完成一个D程序,并且使用dtrace -s或者通过直接添加执行权限来执行时,Dtrace首先会将你的脚本程序编译成一个安全的中间格式(有点类似于Java程序的运行机制),然后才会被加载到内核中执行。Dtrace的执行环境还会检查并处理运行时错误(run-time errors)比如被零除(dividing by zero),访问无效地址等。因此Dtrace是相当安全的。
当Dtrace程序被加载到内核执行时,相应的探测点被启用,如果有涉及探测点的事件发生,我们就把它称之为“触发”,如果此时谓词计算为true,则相应的操作就会被执行。为便于大家理解“启用”和“触发”两个概念,我们举一个日常生活中的实际例子。
现在全国各个城市为了更好地规范交通秩序,都安装了很多“电子警察”(就是“探测器”),安装完成就打开(即“启用”),如果有车闯红灯,就会激活安装在地上的感应线(”触发“),那么”电子警察“就会拍照,很快罚单就会送到你家里(这就是”操作“)。
通过上面这个例子,大家应该有个更加形象的认识了吧。
作为今天的结束,下面是一个监视谁(用户ID)使用什么命令访问一个文件(文件以参数形式传递)的例子。
who_access_thisfile.d
#!/usr/sbin/dtrace -qs
syscall::creat*:return,
syscall::open*:return
/arg0 != -1 && fds[arg0].fi_pathname == $1 /
{
printf("uid#%d %s %s\n",uid,execname,$1);
}
chmod +x who_access_thisfile.d,然后执行./who_access_thisfile.d /etc/passwd,在另一个终端上试试cat /etc/passwd, vi /etc/passwd,看看你都看到了什么信息,你原来能做到吗?
更多的信息,将在下一次中介绍。
星期三 三月 07, 2007
SUN公司的动态跟踪工具DTRACETM真是一个伟大的创举,它使得你可以在Solaris 10及其以上版本的Solaris操作系统中对整个核心的运行情况进行“偷窥”。从此以后,你对系统的运行情况不会再是一头雾水,你可以清晰地知道哪怕是每一条指令的来龙去脉。而且其实现的效率是如此之高,以至于你在没有激活(enable)任何探测点(probe)的时候,你根本不会发现它与之前的Solaris操作系统版本有任何不同。实际上,只要你不是激活了非常多的探测点,其影响也是可以忽略不计的。难怪DTRACE能够脱颖而出荣获《华尔街杂志》2 006技术创新大奖中的金奖。
目前在UNIX/Linux领域,还没有像DTRACE功能如果强大的跟踪技术。Linux有一个仍处于开发阶段的SystemTap项目,主要成员有Red Hat, IBM, Intel, 和Hitachi。但是SystemTap的功能是有限的,它不能跟踪用户程序(至少目前是这样)。下面是IBM中国研发中心一个工程师写的《使用 SystemTap 调试内核》的文章, http://www.ibm.com/developerworks/cn/linux/l-systemtap/index.html
文章中只提到“SystemTap是遵循GPL的开源软件项目”,其实dtrace也已经随着opensolaris的开源而开放出来。 网上还有很多其它的对比DTRACE和SystemTap的文章,比如: http://uadmin.blogspot.com/2006/09/systemtap-vs-dtrace-chart.html
到底DTRACE怎么样,说得太多就会有打广告之嫌,还是自己自己动手用一下。不需要你懂C程序,不需要你读完整个Dtrace Guide,你只需使用DTraceToolkit中oneliners.txt提供的例子就会对它深深着迷!
http://opensolaris.org/os/community/dtrace/ 这是DTRACE的社区
http://docs.sun.com/app/docs/doc/819-6959?l=zh&q=dtrace&a=load 这里有中文版的《Solaris动态跟踪指南》
http://docs.sun.com/app/docs/doc/819-5488?l=en&q=dtrace+guide 这是英文版的
http://www.sun.com/bigadmin/content/dtrace/
还没有安装OpenSolaris?没关系,SUN公司现在正在免费赠送OpenSolaris光盘套件(OpenSolaris Starter Kit)。赶紧去注册吧,机不可失!
明天就是“国际妇女节”,在此预祝全天下的女性朋友们节日快乐,并借此机会感谢我的母亲,我的妻子!
This blog copyright 2009 by samwan
