星期五 七月 24, 2009

      我一直认为,truss是Solaris里面一个非常重要的诊断工具,也是我最喜欢使用的工具。基本上80%的软件问题,我都通过truss找到了线索。Solaris 10提供了更有效的工具dtrace,但是很多时候,truss使用起来更简单,也更容易发现线索(当然,有些问题是truss无法诊断,并且可能导致问题恶化的,比如与时间相关的性能问题等)


      truss可以跟踪进程执行的系统调用(system calls)和信号(signals)情况。其用法如下:


     truss [-fcaeildDE] [- [tTvx] [!] syscall ,...]
          [- [sS] [!] signal ,...] [- [mM] [!] fault ,...]
          [- [rw] [!] fd ,...]
          [- [uU] [!] lib ,... : [:] [!] func ,...]
          [-o outfile] command | -p pid[/lwps]...


      参数比较多,我一般的用法是


      truss -afield -rall -wall -vall -o outputfile -p pid 或者


      truss -afield -rall -wall -vall -o outputfile app


其中outputfile是truss跟踪信息的输出文件,如果不指定-o,缺省会从STDOUT输出,pid是需要跟踪的进程的pid,或者可以直接跟踪一个程序app的执行。


       -rall 表示跟踪所有read()系统调用的I/O buffer的内容


       -wall 表示跟踪所有write()系统调用的I/O buffer的内容


       -vall 表示显示详细信息,比如传递给系统调用的数据结构的信息


       -afield实际上就是-a -f -i -e -l -d的连写,为了便于记忆,你可以把它理解成"一个现场工程师"(field engineer),呵呵。


       这些参数的含义我就不在这里列出,大家请参考man truss


       收集完truss跟踪文件后,我们就可以对跟踪文件进行分析。对此文件的分析需要有一定的系统调用知识。下面是一个具体例子。


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


    [ 问题 ]


    系统无法telnet或者ftp上去(提示连接被拒绝),但是系统的TCP/IP是激活的,可以获得ping响应


    [ 现象 ]


从系统console登录,检查/etc/inetd.conf,有如下内容


ftp     stream  tcp6    nowait  root    /usr/sbin/in.ftpd       in.ftpd
telnet  stream  tcp6    nowait  root    /usr/sbin/in.telnetd    in.telnetd


      inetd进程在运行


   root   952     1  0   May 05 ?       27:39 /usr/sbin/inetd -s
      端口号21,23都在侦听


      *.21                 *.*                0      0 24576      0 LISTEN
      *.23                 *.*                0      0 24576      0 LISTEN


    [ 诊断 ]


      请现场工程师在console里运行:  truss -afield -rall -wall -o /var/tmp/inetd.truss -p 952,


      同时,从另外一台服务器上执行telnet的操作,等到出现连接被拒绝时,Ctrl+C中断上面的truss程序,然后分析inetd.truss


      下面是截取的inetd.truss中关键的信息。(建议从文件结尾往前倒着看,因为问题出现时,我们就中断了truss执行,因此线索应该位于跟踪文件的后半段)


...


22707/1:        44.5740 stat64("/usr/lib/security/pam_seos.so", 0xFFBEF7D0) = 0
22707/1:        44.5741 stat("/usr/lib/security/pam_seos.so", 0xFFBEF10C) = 0
22707/1:        44.5742 open("/usr/lib/security/pam_seos.so", O_RDONLY) = 3
22707/1:        44.5743 fstat(3, 0xFFBEF10C)                            = 0
22707/1:        44.5744 mmap(0x00000000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0xFF1D0000
22707/1:        44.5745 mmap(0x00000000, 950272, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE|MAP_ANON, -1, 0) = 0xFED80000
22707/1:        44.5746 mmap(0xFED80000, 224774, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 0) = 0xFED80000
22707/1:        44.5746 mmap(0xFEDC6000, 33970, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 221184) = 0xFEDC6000
22707/1:        44.5748 mmap(0xFEDD0000, 620336, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANON, -1, 0) = 0xFEDD0000
22707/1:        44.5749 munmap(0xFEDB8000, 57344)                       = 0
22707/1:        44.5751 memcntl(0xFED80000, 74796, MC_ADVISE, MADV_WILLNEED, 0, 0) = 0
22707/1:        44.5751 close(3)                                        = 0
22707/1:        44.5766 munmap(0xFF1D0000, 8192)                        = 0
22707/1:        44.5767 open("/etc/seos.ini", O_RDONLY)                 Err#2 ENOENT
22707/1:        44.5769 open("/usr/seos/seos.ini", O_RDONLY)            Err#2 ENOENT
22707/1:        44.5770 open("/usr/local/seos/seos.ini", O_RDONLY)      Err#2 ENOENT
22707/1:        44.5771 open("/usr/seos/data/seos.ini", O_RDONLY)       Err#2 ENOENT
22707/1:        44.5772 open("/usr/local/seos/data/seos.ini", O_RDONLY) Err#2 ENOENT
22707/1:        44.5774 open(".//seos.ini", O_RDONLY)                   Err#2 ENOENT
22707/1:        44.5776 open(".//etc/osver", O_RDONLY)                  Err#2 ENOENT
22707/1:        44.5777     Incurred fault #6, FLTBOUNDS  %pc = 0xFF19131C
22707/1:              siginfo: SIGSEGV SEGV_MAPERR addr=0x0000000C
22707/1:        44.5778     Received signal #11, SIGSEGV [default]
22707/1:              siginfo: SIGSEGV SEGV_MAPERR addr=0x0000000C

...


      此段大概位于truss跟踪文件的98%位置处。


      注意看粗体部分。


     /usr/lib/security/pam_seos.so 这个动态连接库应该与PAM(Pluggable Authentication Module)模块有关,注意它的pam_前缀。seos应该是CA eTrust的产品。应该是用户对此系统作了安全加固。查看pam.conf,最后几行如下:


#
# eTrust integrate auth and account
#
other   auth    optional        /usr/lib/security/pam_seos.so
rlogin  auth    optional        /usr/lib/security/pam_seos.so
other   account optional        /usr/lib/security/pam_seos.so


      再看上面truss跟踪文件,从open返回ENOENT错误,可以看出inetd无法找到与pam_seos相干的配置文件seos.ini。那么找不到配置文件,会有什么后果呢?看下面紧接着的SIGSEGV就应该知道,后果很严重,直接导致这个22707的子进程终止


22707/1:                *** process killed ***


     22707执行的是/bin/login程序(从truss跟踪文件可以得到此信息,在此略去),由于/bin/login无法执行,因此用户无法使用telnet/ftp登录。


   [ 解决办法 ]


    1. 注释掉/etc/pam.conf中与pam_seos相关的行


    2. 恢复seos.ini文件。


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



星期三 六月 06, 2007

       以前就听美金提起过Unix体验中心(http://www.unix-center.net/),但一直没用上去看看。今天逛了一下,发现已经投入运行的系统还不少呢。

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

T1000/Solaris系统:

硬件环境:1 颗UltraSPARC T1芯片,CPU 主频为1.0 GHz,八核四线程配置8 GB内存

软件环境:Solaris 10 Update 3 for SPARC

机器域名:t1000.unix-center.net(公网),t1000-edu.unix-center.net(教育网)


X4100/Solaris系统:

硬件环境:2 颗双核单线程的AMD Opteron 280芯片,CPU 主频为2.4 GHz,配置4 GB内存

软件环境:Solaris 10 Update 3 for x86/x64

机器域名:x4100.unix-center.net(公网),x4100-edu.unix-center.net(教育网)


PE860/Solaris系统:

硬件环境:1 颗双核单线程的Intel Xeon 3050芯片,CPU 主频为2.13 GHz,配置1 GB内存

软件环境:Solaris 10 Update 3 for x86/x64

机器域名:solaris.unix-center.net(公网),solaris-edu.unix-center.net(教育网)


PE860/Fedora系统:

硬件环境:1 颗双核单线程的Intel Xeon 3050芯片,CPU 主频为2.13 GHz,配置1 GB内存

软件环境:Fedora Core 6

机器域名:fedora.unix-center.net(公网),fedora-edu.unix-center.net(教育网)


PE860/Ubuntu系统:

硬件环境:1 颗双核单线程的Intel Xeon 3050芯片,CPU 主频为2.13 GHz,配置1 GB内存

软件环境:Ubuntu 6.10

机器域名:ubuntu.unix-center.net(公网),ubuntu-edu.unix-center.net(教育网)


PE860/FreeBSD系统:

硬件环境:1 颗双核单线程的Intel Xeon 3050芯片,CPU 主频为2.13 GHz,配置1 GB内存

软件环境:FreeBSD 6.2

机器域名:freebsd.unix-center.net(公网),freebsd-edu.unix-center.net(教育网)


龙芯福珑系统:

硬件环境: 3 台配置龙芯2E处理器的龙芯福珑计算机,CPU 主频为666 MHz,配置256 MB内存

软件环境:Debian Linux for MIPS

机器域名:仅限内网连接


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

      值得自豪的是这里有两台SUN的服务器,有三台运行的是Solaris 10操作系统。当然,我也为有使用我国自主研发的龙芯处理器的龙芯福珑电脑感到骄傲和自豪。

      想找个UNIX开发测试环境的朋友不妨试试。
 

 

星期五 五月 11, 2007

        JavaFX是SUN公司最新的Java创新产品。它可以帮助消费者电器开发商加速基于Java的产品开发。JavaFX提供了一个完全开放的源代码平台。

        下面是一个运行JavaFX Mobile软件的手机:

        JavaFX_Mobile
        更多关于JavaFX的信息,请参阅我们CEO的Blog,以及JavaFX Mobile的主页

 

 
 

星期三 五月 09, 2007

      [ English version ]

       一直说要写一篇文章来介绍我们公司的Perl牛人 Bill Gou写的perfmon软件却一直没有动,今天不能再拖了。

      perfmon是一套用Perl编写的软件包,收集系统性能数据(sar/netstat/ps),并绘制出直观的性能曲线图(日报/周报/月报或者用户定制的周期)。自从我加入到SUN公司,我就没有看到过类似的软件(不管是公司的,还是外部的),B哥的这套工具,可以说是填补了这方面的空白。据我所知,对于IBM的AIX操作系统,有一个这样的工具,叫做nmon(Nigel's Monitor),因此,我们都开玩笑的对B哥说,应该把perfmon改个名字,叫bmon :)

       perfmon使用GNUplot软件来绘制图形,根据你制定的数据集,perfmon可以绘制以下图形(点击连接查看图形,有的图形暂缺):

       所有图形都以PNG(Portable Network Graphics)格式保存。

       要使用perfmon非常简单,只需按照下面的步骤执行:

一、安装必须的软件

      由于要使用GNUplot绘图,因此必须先安装gnuplot软件。对于Solaris 10的用户,可以跳过这一步,因为Solaris 10已经预装了gnuplot程序(/opt/sfw/bin/gnuplot)。对于Solaris 8/Solaris 9的用户,可以从http://www.sunfreeware.com下载安装软件包。同时要下载的还有libpng软件包,否则gnuplot运行时可能会出现找不到动态连接库libpng.so.3的错误。

      Solaris 8的用户,还需要安装sar和sadc的补丁110941-03或更高版本(对于其他OS版本,也建议升级到最新的补丁)。

二、安装并配置perfmon

      1.创建perfmon用户(如果你的系统上已经有此用户名,你可以选择用其它名字。建议此用户只用户perfmon使用)

      2.下载最新的perfmon到你的perfmon用户目录下。

      3. 将最新的perfmon-x.x.x.tar.gz解压解包(升级也使用此方式,配置文件不会被覆盖):gzip -dc perfmon-x.x.x.tar.gz|tar xvf -  

      4. 编辑配置文件 perfmon.conf,所有配置项都是以'item=value'的格式定义。配置项见下表:

配置项
说明
datadir存放数据的目录,缺省是perfmon用户目录下的data目录
sadc
/usr/lib/sa/sadc
sar
/usr/bin/sar
gnuplot
gnuplot程序的全路径名,如:/usr/local/bin/gnuplot,或者/opt/sfw/bin/gnuplot(具体情况可能有所不同)
uname
/usr/bin/uname
netstat
/usr/bin/netstat
ps
/usr/bin/ps
plotset
需要绘制的图形集。有效的图形集为:cpu,mem,paging,swap,io,ps,net,net2.pkt,net2.kbytes,net2.size。多个图形集以逗号(,)分隔。
netif
包含在图形中的网络接口。多个网络接口以逗号(,)分隔
psuser
需要绘制进程数量图的用户。多个用户以逗号(,)分隔,total代表所有用户。
retention
数据保留的时间。具体视空间增长情况而定,缺省是40天。
compress
缺省bzip2
daily_interval
绘制日报图形的时间间隔。缺省为default,即采用与采样相同的时间间隔。有效的时间间隔为数字加上h(表示小时,比如1h),或者加上m(表示分钟,比如10m),或者加上s(表示秒,比如30s)。下同
weekly_interval
绘制周报图形的时间间隔。缺省为30m
monthly_interval
绘制月报图形的时间间隔。缺省为2h


        设置一定要正确,否则可能无法绘制图形。

       5. 修改perfmon.rc中的下面一行:

         /usr/bin/su perfmon -c "/export/share/perfmon/perfreset"

         将perfreset设置为实际的碌径。

        6. 拷贝perfmon.rc到/etc/init.d,并在/etc/rc2.d下面建立一个符号联接:

           #cp perfmon.rc /etc/init.d/perfmon

           # ln -s /etc/init.d/perfmon  /etc/rc2.d/S21perfmon

           启动perfmon:  #/etc/init.d/perfmon start

        7. 编辑perfmon的crontab,指定其绘制日报/周报/月报的具体时间。例子见perfmon随带的README文件。

        配置完perfmon之后,就可以在相应的<datadir>/plot下面找到日报(daily)/周报(weekly)/月报(monthly)的图形。

三、离线使用(手动绘制方式)

       perfmon还提供了手动绘制方式,你只需要使用相应的命令即可:

        perfplot - 绘制日报

        perfplot_weekly - 绘制周报

        perfplot_monthly - 绘制月报

        perfplot_range - 绘制指定的起止区间图形。

        上述命令都可以使用 -h 开关获得使用说明。注意,仅当data目录下有相应的数据文件时,才能绘制出图形。

四、Troubleshooting

      如果你的perfmon没有图形产生,或者产生的图形有问题。你应该首先查看perfmon用户的mail是否有错误信息,是否缺少程序或者动态连接库。perfplot程序在绘制完图形后,会删除相应的临时文件,为了保留临时文件,可以通过设置环境变量PERFMON_DEBUG=1,再运行相应的perfplot程序,然后把错误信息和临时文件收集起来以被诊断。

五、有更好的建议或者其他问题

      perfmon以GPL(GNU Generic Public License)发布,如果你希望对该软件进行改善,或者合作,定制,请联系软件作者:Bill.Gou-AT-Sun-DOT-COM  

      听B哥说正在准备2.0,新版将采用更好的kstat技术(netstat可能导致TCP/IP栈进入串行操作方式,在网络高负荷的情况下,可能导致性能问题)及其他新的features,热切期待中。



 

 


 

         
 

 

 

 



 

星期三 五月 02, 2007

         LDoms 1.0正式版终于发布了。大家可以通过访问http://www.sun.com/ldoms了解更多信息。

星期二 四月 24, 2007

    今天向大家介绍一款超级快的源代码检索和交叉引用工具-OpenGrok

    opengrok

     OpenGrok全部用Java编写的,完全与平台无关。要使用OpenGrok,需要先安装以下软件:

    如果只是用于浏览源代码,则不需要最后两项。

    目前最新的opengrok是opengrok-0.4.tar.gz (4M) 。

    到网上找了半天,居然没有找到可以下载Opensolaris source code的地方,http://opensolaris.org/os/downloads/on/ 上的option 1和option 4给出的http连接里面就没有on-src文件,Option 2给的一个bittorrent文件又是"NOT FOUND",option 3: Mercurial简直太慢,另外一个subversion我就没有再试了。还好昨天收到了Starter kit,光盘上有,不用花精力去找了。

    在这里,我以在Opensuse 10.2上安装及配置 GlassFish + OpenGrok + OpenSolaris O/N source为例。(对于其它平台也类似,因为相关程序都是java编写的)。

    首先将源代码解压到一个目录,比如:/export/home/solsource/usr/src

[安装及配置opengrok]

    安装:tar zxvf opengrok-0.4.tar.gz

    run.bat/run-quiet.sh/run.sh可用于定时运行以更新源代码的索引文件(index database)。需要修改这些文件里面的路径(比如SRC_ROOT,DATA_ROOT等)以反映你的实际情况。

    由于我不会更新源代码,因此我只用调用一次opengrok.jar创建索引文件即可。

    opengrok.jar有两种调用模式:

  • 第一种,启用图形界面: java -jar opengrok.jar

    程序启动后,会显示一个主窗口

    OpenGrok Main Window
 

    点击'Search'文本框右面的文件夹图标打开一个新窗口,

    Index
 

    选择相应的'Search index data directory'和'Source tree directory',点击'update'按钮,此时会出现正在更新的信息和进度条,

   
 

    索引数据库更新完成后,会显示下面的信息。

   
 

    点击'Close'以返回主窗口。

    此时你就可以在相应文本框中输入关键字进行查找。双击查找结果就会打开一个文本编辑器(但是注意,在文本编辑器中就没有交叉引用及查找功能,如果你需要打开的每个文件都具有交叉引用的信息,那么你就需要继续下面的glassfish配置)

  • 第二种,命令行: java -jar opengrok.jar -s SRC_ROOT DATA_ROOT 

     SRC_ROOT和DATA_ROOT与第一种方式一样。

     为了配合glassfish的使用,需要修改source.war文件:

 

[安装及配置glassfish]

    glassfish

    从https://glassfish.dev.java.net/网站下载最新的glassfish,是一个后缀名为jar的软件包。用java程序安装此软件包:

    java -jar glassfish-installer-v2-b33e.jar

    会显示CDDL 1.0 License信息,回答'A',就会自动解包,当显示"installation complete",表示安装完成,此时在当前目录下会产生一个glassfish的目录。

    在设置glassfish之前,你需要设置以下环境变量:

 

  • JDK_HOME
  • JRE_HOME
  • JAVA_BINDIR
  • JAVA_HOME
  • JAVA_ROOT

    这些变量应该与你安装的最新的java的路径相对应。

    为了设置glassfish,我们还需要用ant工具。glassfish软件包自带了一个ant,在glassfish/lib/ant目录下面,因此我们要设置ANT_HOME指向这个glassfish/lib/ant(注意,这里只是相对路径,你要根据你的情况更改);另外我们还需要给$ANT_HOME/bin/ant加上执行权限。

    然后运行命令: $ANT_HOME/bin/ant -f setup.xml

    ant就会根据setup.xml进行编译。编译如果成功,会显示如下信息:

    BUILD SUCCESSFUL
    Total time: 35 seconds

    注意:你的编译时间可能有所不同。

    你可能已经注意到了编译过程中输出的缺省的端口号设置:

  create.domain:
     [exec] Option adminuser deprecated, use --user instead.
     [exec] Using port 4848 for Admin.
     [exec] Using port 8080 for HTTP Instance.
     [exec] Using port 7676 for JMS.
     [exec] Using port 3700 for IIOP.
     [exec] Using port 8181 for HTTP_SSL.
     [exec] Using default port 3820 for IIOP_SSL.
     [exec] Using default port 3920 for IIOP_MUTUALAUTH.
     [exec] Using default port 8686 for JMX_ADMIN.

     现在我们就可以启动web server。进入glassfish/bin目录,执行:./asadmin,

     Use "exit" to exit and "help" for online help.
     asadmin> 输入start-appserv

     当你看到如下信息时,表示appserver启动成功:

     Domain listens on at least following ports for connections:
     [8080 8181 4848 3700 3820 3920 8686 ].

     启动浏览器访问http://localhost:8080/看是否有"Your server is up and running!"的信息。

     访问http://localhost:4848/,输入用户名: admin 密码: adminadmin (缺省)

     在右面的"Common Task"页面上点击"Deploy Web Application(.war),然后在"Deploy Enterprise Applications/Modules"页面"Location"处选择"Packaged file to be uploaded to the server",点击"Browse",选中在“配置opengrok”任务中编辑好的source.war文件,点击"open",然后点击右上角的"OK"按钮,你就完成了opengrok的布局。

     现在你就可以在你的浏览器中输入http://localhost:8080/source/开始你的源代码之旅了。

     现在就下载glassfish并填写调查表,说不定你还有机会中一个iPod Nano呢。心动不如行动!

 

   
 

    

 

星期一 四月 23, 2007

      今天收到了我订购的OpenSolaris starter kit,迫不及待地打开,光盘做得相当漂亮。一共有两张DVD:

    DVD
 

      < disc 1 >包括:

  • Learning Materials
  • Belenix LiveCD
  • Schilix LiveCD
  • Nexenta LiveCD
  • Nexenta Install
  • OpenSolaris Source

      < disk 2 > 是: Solaris Express Community Edition b57

      我觉得disk 1做得真是太好了,简直就是All-in-ONE。而且很好用,你只用从这张光盘启动,就可以选择任何一个Opensolaris的分发版来使用:

      opensolaris starterkit boot

   

      世界上最好的操作系统Opensolaris的精华都在这两张DVD上面了。还没有订购的朋友们,赶紧到这里http://www.opensolaris.org/kits/订购,完全FREE。

       

 

星期五 四月 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,看看你都看到了什么信息,你原来能做到吗?

      更多的信息,将在下一次中介绍。

    
  

     
 

星期四 三月 15, 2007

      今天在我的LAPTOP上玩了一下VirtualBox上跑Belenix,感觉还不错。

      VirtualBox是InnoTek公司开发的一款虚拟机软件,它是按照GNU Public License (GPL)进行分发的。VirtualBox比Xen功能强大并且具有更为友好的用户界面,同时它本身又比VMWare少用系统资源。是一款不可多得的开源虚拟机软件。

       Belenix是基于OpenSolaris的Live CD(可直接从光盘运行),非常适合用来测试OpenSolaris。

       我的LAPTOP配置了1G内存,安装的是Slackware Linux(current),内核是2.6.15.2,我分配了512M内存给VirtualBox跑Belenix。我是从VirtualBox网站下载的针对Linux所有发行版的最新1.3.8版的安装文件。

       安装非常简单:

       1. chmod +x VirtualBox_1.3.8_Linux_x86.run

       2. ./VirtualBox_1.3.8_Linux_x86.run install  (卸载使用uninstall参数)

      安装过程中会根据你当前的内核编译模块vboxdrv.ko,模块会放在 /lib/modules/`uname -r`/misc/下面,另外会生成一个启动脚本,在Slackware下是/etc/rc.d/rc.vboxdrv,同时你需要把你的用户加入到vboxusers组中。

      启动时,首先运行启动脚本 /etc/rc.d/rc.vboxdrv start,启动脚本会产生相应的设备文件/dev/vboxdrv,并加载模块vboxdrv.ko到核心里,然后你就可以使用VirtualBox命令,之后的操作就像使用VMWare一样简单了。

       下面是一些屏幕截图的链接

 

        大家感兴趣可以自己试试! :)

     
      

      Snap  Preview Anywhere[tm] 是
Snap公司提供的一个免费的显示网页超链接缩略图的软件。你只要在其主页http://www.snap.com/登记,你就可以向你的网页中添加script来获得缩略图功能。这样当别人访问你的网页时,只要他或她把鼠标放在一个链接上,一会儿就能看到这个链接网页的缩略图,就像你刚才把鼠标放在snap的主页上一样。是不是很酷?

  

星期一 三月 12, 2007

        ZFS是SUN公司开发的业界第一个128位的文件系统。它具有以下优点:

1. 可证实的数据完整性

      数据完整性是所有用户都关心的头等大事,如果存储的数据都不完整了,那还有存储干什么呢?有人说,不用ZFS,我有RAID-5啊。说这话的朋友可能不知道RAID-5有个致命的问题--写漏洞(write hole)。因为RAID-5在写数据和校验信息时不是原子操作(atomicity),就可能出现新的数据已经写到磁盘上,但是新的校验信息还没有写完的情况,比如整个磁盘阵列异常掉电时,这样当下次读取这块数据时,就会得到一堆”垃圾“。虽然可以靠添加非易失性存储(NVRAM)来解决这种问题,但这无疑增加了企业的成本。而相比之下,ZFS并不需要添加任何新的硬件。这都要感谢ZFS基于“事务性“(transaction)的面向对象的处理方式。

      ZFS是一种”树状“文件系统,其超级块(uberblock)就是整个文件系统的”根节点“,数据块就是”叶节点“,而地址块就是中间的”枝节点“。如下图所示:

      Intial Block tree
                            图1 初始状态

     其中树的每个节点都是一个对象。ZFS对文件块(除文件系统超级块uberblock)的修改操作都采用”写时拷贝(Copy-On-Write)“技术实现,而从”叶节点”到“根节点”的操作是事务性的,即一个分支要么被修改,要么就什么也没有改变。

     下面是对数据块data1和data2进行修改的示意图:

     cow data1 and data2

                          图2 对data1和data2数据块作“写时拷贝(COW)”
       cow indirect block

                          图3 对间接地址块作“写时拷贝(COW)”

      rewite uberblock
                        图4 修改超级块(uberblock)

      尽管这里修改数据块data1和data2有4个图,实际上从图1到图4的操作是“事务性的原子操作“,即最终的结果要么就是图1的状态,要么就是图4的状态,不可能是中间状态。

      由于每个数据块有256位的校验码(checksum),在遍历数据的过程中,ZFS会使用数据块的校验码校验数据的正确性,一旦发现有错,ZFS就能自行进行纠正(self-healing)。这样就绝对的保证了数据的完整性。

2. 卓越的性能

      ZFS采用了一种称为"RAID-Z"的技术,"RAID-Z"与RAID-5类似,也是数据和校验相结合的模式,但是与RAID-5不同的是,ZFS使用可变的条带宽度(Dynamic Stripe Width),每一个数据块就是一个条带,因此,ZFS的每次写操作都是一个完整的条带写操作,这样就不需要像RAID-5一样作”读-修改-写“,再加上ZFS的”写时拷贝“技术,可以将随机写操作转换为顺序写操作并且在多个磁盘上并发的完成,因此,ZFS的性能比传统的RAID技术要好很多。

3. 简单的管理

       ZFS本身就集成了卷管理(Storage Pool)的功能,不需要你在卷管理软件和文件系统之间折腾,甚至不需要你修改/etc/vfstab和/etc/dfs/dfstab,因为ZFS可以自动安装及共享文件系统。你会发现你是否真的还需要花钱去雇人来对你的文件系统进行管理,因为这一切已经被ZFS变成一件愉快的事。

        创建一个镜像池,你只需要:

        # zpool create home mirror c0t0d0 c1t0d0

        创建一个文件系统,

       # zfs mount -c home/user1 /export/home/user1

       更多的例子,你可以在《ZFS: the last word in file systems》中找到

4. 巨大的存储量

      正如文章开篇说所,ZFS是128位的文件系统。这里128位指的是ZFS可以寻址的数据块的地址位数。每个数据块是512个字节,即2^9bytes=2^12bits,因此一个ZFS可以存储(2^128)*(2^12)=2^140bits。到底这个2^140bits是什么样一个概念,可能很多朋友都不知道,下面引用ZFS的开发者Jeff Bonowick的Blog上一个形象的例子来告诉大家。

 


      大家都知道摩尔定律,但是摩尔定律忽略了一点,那就是任何物质都有其上限。根据Seth Lloyd的研究,1公斤的物质(1升的空间内)其极限是每秒执行10^51次运算,存储10^31位(bit)信息,而一个存满了的ZFS可以存储2^140位信息,因此至少需要(2^140bits)/(10^31bits/kg)=136 billion公斤物质。

      为了将这个”设备“运行起来,它必须是以纯能量的方式。按照爱因斯坦E=mc^2,需要的能量是1.2x10^28焦耳。海洋的质量是1.4x10^21公斤。让1公斤水温度升高1摄氏度需要4000焦耳能量,因此让1公斤水从凝固到沸腾(100摄氏度)需要400000焦耳能量,再加上每公斤蒸发需要的2000000焦耳能量。蒸发整个海洋共需要(2000000+400000)焦耳/公斤*1.4x10^21公斤=3.4x10^27焦耳。可见要运行一个存满ZFS的设备需要的能量远超过把海洋蒸发完的能量。

 



       现在你该相信ZFS就是"the last word in filesystem”了吧。

5. ZFS是开源的(Open)

       秉承SUN的开源思想,ZFS项目也是开源的。如果你是软件开发人员并且对文件系统感兴趣的话,你可以加入到ZFS的项目中:http://www.opensolaris.org/os/community/zfs

6. ZFS是免费的

       ”天下没有免费的午餐“,这句话在SUN公司就不成立了。有了功能如此强大的ZFS,作为公司的决策者,你还有必要花钱去单独买昂贵的卷管理软件吗?

 

 

 




 


 

     
 


 

星期六 三月 10, 2007

      今天在网上闲逛,看到了一篇赵柯翻译的Linux上的虚拟化技术”,特与大家分享。尽管标题上写的是Linux,实际上虚拟化技术的使用相当广泛。

     
 

     
 

This blog copyright 2009 by samwan