星期三 十二月 14, 2011

A revised execsnoop

samexecsnoop is a revised dtrace script based on execsnoop which was written when I was learning dtrace in 2008.


It includes below changes

--------------------------------------

# 25-Apr-2008   Sam     Wan     Add field for Login uid and change uid to ruid, add euid
# 30-Apr-2008   Sam     Wan     Add absolute executable path

----------------------------------------


Although still a lot to be improved. I now upload it here in case someone may find it's useful for his/her daily work.


Also with a chinese version doc on it

星期二 十二月 13, 2011

a dtrace example to troubleshoot 'cp -r' hang


Yesterday when I was tring some test on a zpool, I found a problem and then solved it with dtrace.

The test was a simple 'cp -r /etc /tank' where /tank is a zfs i created. However soon after the command was started, it just hang there.
===============================================================
root@vbs10 # df -h /tank
Filesystem             size   used  avail capacity  Mounted on
tank                    31G    31K    31G     1%    /tank
root@vbs10 # cp -r /etc /tank

===============================================================

df -h /tank showed that it almost got no growing after a while.
================================================================
root@vbs10 # df -h /tank
Filesystem             size   used  avail capacity  Mounted on
tank                    31G   824K    31G     1%    /tank
================================================================

Let's check the stack backtrace of the cp process.
=================================================================
root@vbs10 # ps -ef|grep cp
    root  2032  1964   0 10:04:57 pts/2       0:00 cp -r /etc /tank
root@vbs10 # pstack 2032
2032:   cp -r /etc /tank
 feefb597 read     (4, 803e1a0, 8000)
 08052bd9 ???????? (4, 5, 8046b40, 8068e58, 80662e0, 8066370)
 080526ab ???????? (8046b40, 8068e58)
 08053678 ???????? (8047d66, 8067100)
 08053b03 ???????? (8047d66, 8067100)
 08052777 ???????? (8047d66, 8067100)
 08051b73 main     (1, 8047c98, 8047ca4) + 430
 08051696 ???????? (4, 8047d60, 8047d63, 8047d66, 8047d6b, 0)
root@vbs10 #
=======================================================================

looks like it's waiting for some 'read' to complete.

well, take a closer look by mdb
======================================================================
root@vbs10 # mdb -p 2032
Loading modules: [ ld.so.1 libc.so.1 libavl.so.1 libcmdutils.so.1 ]
> $C
0803e17c libc.so.1`_read+7(4, 803e1a0, 8000)
080461b8 0x8052bd9(4, 5, 8046b40, 8068e58, 80662e0, 8066370)
08046a00 0x80526ab(8046b40, 8068e58)
08046f50 0x8053678(8047d66, 8067100)
08047010 0x8053b03(8047d66, 8067100)
08047848 0x8052777(8047d66, 8067100)
08047c64 main+0x430(1, 8047c98, 8047ca4)
08047c84 _start+0x7a(4, 8047d60, 8047d63, 8047d66, 8047d6b, 0)
> libc.so.1`_read+7::dis
libc.so.1`_read:                movl   $0x3,%eax
libc.so.1`_read+5:              syscall
libc.so.1`_read+7:              jae    +0xc     <libc.so.1`_read+0x13>
libc.so.1`_read+9:              cmpl   $0x5b,%eax
libc.so.1`_read+0xc:            je     -0xc     <libc.so.1`_read>
libc.so.1`_read+0xe:            jmp    -0x8abee <libc.so.1`__cerror>
libc.so.1`_read+0x13:           ret
0xfeefb5a4:                     addb   %al,(%eax)
0xfeefb5a6:                     addb   %al,(%eax)
0xfeefb5a8:                     addb   %al,(%eax)
0xfeefb5aa:                     addb   %al,(%eax)
0xfeefb5ac:                     addb   %al,(%eax)
0xfeefb5ae:                     addb   %al,(%eax)
>
======================================================================

yes it's stuck in libc.so`_read.

My idea was to find out what's the file being read at that time.

Well the best tool I can think of is the opensnoop in DTraceToolkit

Fortunately the issue was reproducible, so I did it again, this time with opensnoop.

I ran 'opensnoop -n cp' on one terminal and then 'cp -r /etc /tank' on another terminal.

==================================================================================
...

    0   2269 cp             8 /etc/webconsole/console/prereg/console/userlogin.reg
    0   2269 cp             8 /etc/webconsole/console/prereg/console/jato_jar.reg
    0   2269 cp             6 /etc/webconsole/console/config.properties
    0   2269 cp             6 /etc/webconsole/console/.pswd
    0   2269 cp             6 /etc/webconsole/console/status.properties
    0   2269 cp             7 /etc/webconsole/console/regcache/cache.bkp
    0   2269 cp             7 /etc/webconsole/console/regcache/registry.properties
    0   2269 cp             6 /etc/webconsole/console/default.properties
    0   2269 cp             4 /etc/utmppipe

...
=================================================================================

 The last file being accessed was '/etc/utmppipe' and yes cp was stuck here.

 So what's it?

================================================================
root@vbs10 # ls -lh utmppipe
prw-------   1 root     root           0 Dec 12 17:31 utmppipe
================================================================

 It's a pipe file.


 That's it. When you 'cp -r' on a pipe file, it tries to read from pipe instead of replicating the pipe file itself and this keeps it waiting.

  Let's see if there's any option regarding pipe in cp manpage. And indeed there's one

=======================================================================
     ‐R    Same as ‐r, except  pipes  are  replicated,  not  read from.
=======================================================================

   Now we know that the problem can be overcome by using 'cp -R /etc /tank' instead of 'cp -r /etc /tank'

====================================================================
root@vbs10 # time cp -R /etc /tank
cp: cannot create special file /tank/etc/utmppipe: File exists
cp: /etc/.syslog_door: read: Operation not applicable
cp: /etc/sysevent/syseventconfd_event_channel/reg_door: read: Operation not applicable
cp: /etc/sysevent/devfsadm_event_channel/1: read: Operation not applicable
cp: /etc/sysevent/devfsadm_event_channel/reg_door: read: Operation not applicable
cp: /etc/sysevent/piclevent_door: read: Operation not applicable
cp: /etc/sysevent/sysevent_door: read: Operation not applicable
cp: /etc/svc/volatile/repository_door: read: Operation not applicable

real    0m7.095s
user    0m0.331s
sys     0m3.130s

root@vbs10 # du -sh /etc
  70M   /etc

root@vbs10 # df -h /tank
Filesystem             size   used  avail capacity  Mounted on
tank                    31G    80M    31G     1%    /tank
====================================================================

    it only took me 7s.

    This problem may be also investigated by other tools such as truss. However as you can see dtrace is much powerful and quite easy to use especially with the dtracetoolkit 'killer-app'.

-EOF-

星期五 四月 13, 2007

DTRACE简介之完结篇


[Read More]

星期三 三月 21, 2007

DTRACE简介(3)

     在上一篇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)手册页)。
  

 

 


 

DTRACE简介(2)

      通过上一次的介绍,相信大家对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

DTRACE简介(1)

      记得几年前看过一部美国大片叫《全民公敌(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)。赶紧去注册吧,机不可失!

      明天就是“国际妇女节”,在此预祝全天下的女性朋友们节日快乐,并借此机会感谢我的母亲,我的妻子

About

samwan

Search

Categories
Archives
« 四月 2014
星期日星期一星期二星期三星期四星期五星期六
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
今天