星期五 四月 13, 2007


[Read More]

星期四 三月 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

#!/usr/sbin/dtrace -qs
BEGIN
{
     printf("target=%d\n",$target);
     exit(0);
}

     再编写一个测试用的shell脚本,此脚本只打印自己的进程号

pid.sh

#!/bin/sh
echo mypid=$$

     然后我们执行以下操作

 


# 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),以更好的分析数据。如下例,我们按照执行的时间来标准化聚合数据,以得到每秒钟的系统调用数。

#pragma D option quiet
BEGIN
{
     start=timestamp;
     /*获得起始的时间戳*/
}
syscall:::entry
{
     @func[execname]=count();
     /*按执行程序名称聚合系统调用的次数*/
}
END
{
      normalize(@func,(timestamp-start)/1000000000);
      /*退出时间戳减去启动时间戳就是运行的总时间,然后除以1000000000就转换为以秒为单位,再用这个描述去标准化系统掉调用次数的聚合*/
}

      标准化不会修改原始数据。与标准化相对应的是“取消标准化(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 表示推理缓冲区的个数(如果不指定,缺省只有一个)

       /* */ 之间的内容是注释,

#!/usr/sbin/dtrace -s
#pragma D option flowindent
#pragma D option nspec=200
syscall:::entry                    /*syscall提供器的entry探测器,在进入相应的系统调用之前被触发*/
{
    self->spec=speculation();        /*申请一个推理缓冲区*/
    speculate(self->spec);            /*把缺省操作(就是记录当前探测器的信息)的数据放到指定的推理缓冲区*/
}
fbt:::                     /*fbt是函数边界跟踪(Function Boundary Tracing)提供器,它提供了对所有函数的跟踪*/
/self->spec/         /*只针对已经申请了推理缓冲区的线程*/
{
    speculate(self->spec);     /*将函数名放到推理缓冲区*/
}
syscall:::return                  /*syscall提供器的return探测器,在退出相应的系统调用之后被触发*/
/self->spec && arg0 != -1/      /*系统调用的返回值不是-1(表示系统调用成功)*/
{
    discard(self->spec);        /*不是我们关心的情况,丢弃推理缓冲区数据*/
    self->spec=0;                 /* 赋0值,以释放变量空间(养成好习惯)*/
}
syscall:::return                   /*与上一个探测器描述一样(对于同样的探测器描述,可以指定多个子句块)*/
/self->spec && arg0==-1 && errno==EINVAL/    /*返回值为-1并且errno就是EINVAL*/
{
    commit(self->spec);          /*这就是我们想要的信息,因此提交推理缓冲区*/
    committed=1;                   /*赋值一个变量committed,以表示我们已经提交了*/
}
syscall:::return                      /*同上*/
/committed/                           /*是否已提交*/
{      
    exit(0);                               /*如果已提交,就退出DTrace*/
}
syscall:::return                       /*同上*/
/self->spec && arg0==-1 && errno!=EINVAL/    /*返回值为-1但是errno不是EINVAL(可能是其它错误)*/
{
    discard(self->spec);           /*也不是我们关心的,丢弃*/
    self->spec=0;                    /*释放变量空间*/
}

       你还可以将上面程序中的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位机器字长
 char1个字节
 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"进行格式化输出。

#!/usr/sbin/dtrace -qs
syscall::open:entry,
syscall::open64:entry
{
     printf("%s[%d] opened %s\n",execname,pid,arg0);
}

       运行一下看看


 # ./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类型,不匹配。

       正确的程序应该是:

 

#!/usr/sbin/dtrace -qs
syscall::open:entry,
syscall::open64:entry
{
     printf("%s[%d] opened %s\n",execname,pid,copyinstr(arg0));
}

        再来看看

 


# ./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)”模式。如下图所示:

 dtrace architecture

       ”提供器“提供了”探测器“,而”使用者“通过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程序的结构如下:

0      #!/usr/sbin/dtrace -s
1      pragma D option quiet
2      probe_description_1 
3      / predicate_1 /
4     {
5           action_1;
6           action_2;
7             ...
8            action_n;
9      }
10      probe_description_2
11      / predicate_2 /
12     {
13           action_1;
14           action_2;
15             ...
16            action_n;
17      }
... 
18      probe_description_n
19      / predicate_n /
20     {
21           action_1;
22           action_2;
23             ...
24            action_n;
25      }

      上面的伪代码(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