跟踪 UNIX 应用程序的解决方案

如果您正在开发 UNIX® 应用程序,那么可以跟踪和调试正在运行的应用程序,并从程序中提取需要的信息。但是,如果您希望知道 UNIX 应用程序中正在发生的情况,但是无法查看源代码,那该怎么办呢?本教程讨论一些跟踪系统,它们能够帮助您跟踪应用程序的执行情况,了解它们正在做的事情,而不需要对源代码做任何修改,甚至不需要停止并重新启动应用程序。

Martin C Brown, 自由撰稿人和顾问, MCslp

Martin Brown 成为一名职业作家已经有 8 年多的时间了。他是很多书籍和文章的作者,内容涉及很多主题。他的特长包括很多开发语言和平台 —— Perl、Python、Java、JavaScript、Basic、Pascal、Modula-2、C、C++、Rebol、Gawk、Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X 等等 —— 还包括 Web 编程、系统管理和集成。他会定期为 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 撰写文章,在 Computerworld、Apple Blog 以及其他站点都会定期更新自己的 blogger,同时还为 Microsoft 撰写一些主题文章。您可以通过 questions@mcslp.com 与他联系。



2009 年 6 月 03 日

开始之前

本教程帮助 UNIX 系统开发人员和管理员以最佳方式跟踪系统上运行的应用程序。要想从本教程获得最大收获,您应该了解 UNIX 操作系统的基本知识及其操作方式。具备基本的编程经验会有帮助,但不是必需的。

关于本教程

大多数开发人员和系统管理员知道在他们的操作系统和应用程序中应该会发生什么情况,但糟糕的是,有时候不是这样的。当应用程序崩溃或表现异常时,需要查明更多信息。通过利用对应用程序正常工作方式的了解和一些基本 UNIX 技能,可以跟踪应用程序,查明造成问题的原因。本教程讲解使用跟踪工具了解应用程序内部情况的基本技术。

本教程首先讨论调试和跟踪的差异,以及这两种解决方案的工作方式差异。然后,通过一些具体示例讲解如何使用跟踪解决应用程序中的问题。DTrace 提供跟踪和调试两种系统的组件,还支持对应用程序进行计时和基准测试。最后,本教程讲解如何跟踪在网络计算机之间交换的信息,帮助发现网络应用程序中的问题。


跟踪概述

有时候,需要了解在应用程序内部正在发生的情况。例如,应用程序可能会运行失败,而又没有显示有帮助的错误消息,或者系统服务没有按照预期的方式运行。在这些情况下,您可能不掌握应用程序源代码,因此无法通过传统的调试过程寻找问题的原因。跟踪提供了一种替代方法。

调试

对于开发人员来说,寻找 UNIX 应用程序问题的主要方法是,使用开发环境或操作系统的调试特性检查源代码,查明造成问题的原因。

大多数调试系统支持逐行监视和检查代码行的执行过程,还支持监视变量和结构的值。可以使用调试器在代码中设置断点,执行过程会停在断点上;在断点上,可以获得关于调用堆栈(函数的调用路径)的信息以及变量值。

我们来看一个例子,假设一个应用程序根据人的生日计算他的年龄,还要考虑到闰年等因素。要想调试这个应用程序,需要有源代码,还需要在启用调试选项的情况下编译应用程序:$ gcc -g ageindays.c -o ageindays

运行这个应用程序,提供用户的生日和用来比较的目标日期(见清单 1)。

清单 1. 执行比较
$ ./ageindays 24/1/1980 22/2/2009
You have been alive 10622 days
You were born on 24/1/1980 which is a Thursday

在调试应用程序时,首先怀疑问题出在 calc_diff 函数中,这个函数计算第一个和第二个日期的差。接下来,可能按照清单 2 这样进行调试。

清单 2. 调试 calc_diff 函数
$ gdb ageindays
GNU gdb 6.3.50-20050815 (Apple version gdb-962) (Sat Jul 26 08:14:40 UTC 2008)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-apple-darwin"...Reading symbols for shared 
libraries ... done

(gdb) b calc_diff
Breakpoint 1 at 0x1bd7: file ageindays.c, line 27.
(gdb) r 24/1/1980 26/3/2009
Starting program: /nfs/MC/UnixSrc/c/bio/ageindays 24/1/1980 26/3/2009
Reading symbols for shared libraries ++. done

Breakpoint 1, calc_diff (day=26, month=3, year=2009) at ageindays.c:27
27	  unsigned long days_diff=0;
(gdb) bt
#0  calc_diff (day=26, month=3, year=2009) at ageindays.c:27
#1  0x00001e3d in main (argc=3, argv=0xbffff708) at ageindays.c:89
(gdb) p days_diff
$1 = 8041
(gdb)

通过 清单 2 中的输出可以看出,我们打开了调试器,通过指定名称在 calc_diff() 函数中设置了一个断点,然后在调试器中运行程序,提供与命令行相同的参数。

当调试器到达创建的断点时,执行过程停止,您可以检查应用程序代码和调用的函数。通过使用调试器,可以查看提供给函数的参数及其值(在这里是为目标日期提供的日期信息)。执行停止之后,可以查看堆栈跟踪,查看代码中调用 calc_diff 函数的行,可以获得 days_diff 变量的值。因为应用程序的执行过程已经暂停了,所以还可以修改变量的值。这样就可以在应用程序中尝试使用不同的值,从而寻找潜在的问题。

可以使用这些信息,因为定义了特定的调试信息(组成函数和变量名的符号)和其他元数据(比如定义函数的代码行)。

必须在编译时把特定的调试信息添加到二进制应用程序中;更重要的是,必须访问源代码,才能把调试信息包含在编译的应用程序中。如果无法识别函数名和变量,那么几乎不可能调试程序。

跟踪与调试的对比

系统管理员(和开发人员)常常希望发现正在运行的程序中的错误。例如,某个程序为什么造成了其他问题(比如内存和其他错误),应用程序的表现为什么不符合预期,它过去发生了什么情况。在这种情况下,调试应用程序的特定方面往往没什么用。需要查明的实际上是操作系统如何执行应用程序。

在进行调试时,检查的是应用程序中定义的各个函数的执行过程。调试主要关注应用程序本身,包括其中的函数和结构,通常会忽视应用程序向操作系统发出的系统调用和库函数调用。调试能够提供关于应用程序的大量信息,但是对于了解操作系统如何执行应用程序帮助不大。

在进行跟踪时,监视应用程序和操作系统之间的交互,常常会检查应用程序在执行期间调用的操作系统函数。

除了这些差异之外,跟踪和调试之间的主要差异是,跟踪不要求访问源代码,也不要求以任何特殊方式编译应用程序。这意味着可以跟踪操作系统附带的或第三方厂商提供的应用程序。

通过跟踪应用程序,可以查明以下方面的情况:

  • 内存使用量和映射内存的调用
  • 在执行期间打开和关闭的文件
  • 对不同文件的读和写操作
  • 为给定的应用程序装载的库

下面先研究一下 truss 的输出,truss 是一种可以在 Solaris 和 AIX® 上使用的工具。


使用 truss 和 strace

在 Solaris 和 AIX 上可以使用 truss 工具,它能够跟踪应用程序中的系统调用和信号。可以在 Linux® 上使用的 strace 工具提供相似的功能。在不同的系统上,还有提供相似信息的其他工具,包括 ktrace (FreeBSD) 和 trace。

truss/strace 概述

truss 和 strace 工具提供相似的信息,但是命令行选项稍有差异。使用这两种工具的标准方法都是把工具名放在要执行的命令前面。

例如,清单 3 给出 truss 对于本教程前面提到的 ageindays 程序的输出。

清单 3. truss 的输出
$ truss ./ageindays 24/1/1980 26/3/2009
execve("ageindays", 0x08047BBC, 0x08047BCC)  argc = 3
mmap(0x00000000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, -1, 0) 
   = 0xFEFB0000
resolvepath("/usr/lib/ld.so.1", "/lib/ld.so.1", 1023) = 12
getcwd("/root", 1013)                           = 0
resolvepath("/root/ageindays", "/root/ageindays", 1023) = 15
xstat(2, "/root/ageindays", 0x08047880)         = 0
open("/var/ld/ld.config", O_RDONLY)             = 3
fxstat(2, 3, 0x08047760)                        = 0
mmap(0x00000000, 144, PROT_READ, MAP_SHARED, 3, 0) = 0xFEFA0000
close(3)                                        = 0
sysconfig(_CONFIG_PAGESIZE)                     = 4096
xstat(2, "/usr/lib/libc.so.1", 0x08046FA0)      = 0
resolvepath("/usr/lib/libc.so.1", "/lib/libc.so.1", 1023) = 14
open("/usr/lib/libc.so.1", O_RDONLY)            = 3
mmap(0x00010000, 32768, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_ALIGN, 3, 0) 
    = 0xFEF90000
mmap(0x00010000, 1413120, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE|MAP_ANON|MAP_ALIGN, -1, 0)
    = 0xFEE30000
mmap(0xFEE30000, 1302809, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_TEXT, 3, 0) 
    = 0xFEE30000
mmap(0xFEF7F000, 30862, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|
    MAP_INITDATA, 3, 1306624) = 0xFEF7F000
mmap(0xFEF87000, 4776, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANON, 
   -1, 0) = 0xFEF87000
munmap(0xFEF6F000, 65536)                       = 0
memcntl(0xFEE30000, 187632, MC_ADVISE, MADV_WILLNEED, 0, 0) = 0
close(3)                                        = 0
mmap(0x00010000, 24576, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON|MAP_ALIGN, 
   -1, 0)
    = 0xFEE20000
munmap(0xFEF90000, 32768)                       = 0
getcontext(0x080475D0)
getrlimit(RLIMIT_STACK, 0x080475C8)             = 0
getpid()                                        = 15691 [15690]
lwp_private(0, 1, 0xFEE22A00)                   = 0x000001C3
setustack(0xFEE22A60)
sysi86(SI86FPSTART, 0xFEF879BC, 0x0000133F, 0x00001F80) = 0x00000001 
ioctl(1, TCGETA, 0x08046C20)                    = 0
fstat64(1, 0x08046B80)                          = 0
You have been alive 10654 days
write(1, " Y o u   h a v e   b e e".., 31)      = 31
You were born on 24/1/1980 which is a Thursday
write(1, " Y o u   w e r e   b o r".., 47)      = 47
_exit(134511508)

清单 4 给出 Linux 上 strace 的输出。

清单 4. strace 的输出
$ strace ./ageindays 24/1/1980 26/3/2009
execve("./ageindays", ["./ageindays", "24/1/1980", "26/3/2009"], 
[/* 50 vars */]) = 0
brk(0)                                  = 0x602000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 
    = 0x7f47db185000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
    = 0x7f47db184000
access("/etc/ld.so.preload", R_OK)      
    = -1 ENOENT (No such file or directory)
open("/usr/lib/tls/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/lib/tls/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/lib/tls/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/lib/tls", 0x7fffe31858f0)    
    = -1 ENOENT (No such file or directory)
open("/usr/lib/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/lib/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/lib/libc.so.6", O_RDONLY)    
    = -1 ENOENT (No such file or directory)
stat("/usr/lib", {st_mode=S_IFDIR|0755, st_size=53248, ...}) = 0
open("/usr/local/lib/tls/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/lib/tls/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/lib/tls/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/lib/tls", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/lib/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/lib/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/lib/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/lib", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
open("/usr/local/qt/lib/tls/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/qt/lib/tls/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/qt/lib/tls/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/qt/lib/tls", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/qt/lib/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/qt/lib/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/local/qt/lib/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/local/qt/lib", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/X11R6/lib/tls/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/X11R6/lib/tls/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/X11R6/lib/tls/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/X11R6/lib/tls", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/X11R6/lib/x86_64/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/X11R6/lib/x86_64", 0x7fffe31858f0) 
    = -1 ENOENT (No such file or directory)
open("/usr/X11R6/lib/libc.so.6", O_RDONLY) 
    = -1 ENOENT (No such file or directory)
stat("/usr/X11R6/lib", 0x7fffe31858f0)  
    = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=85050, ...}) = 0
mmap(NULL, 85050, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f47db16f000
close(3)                                = 0
open("/lib/libc.so.6", O_RDONLY)        = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\345\1\0\0\0\0\0@"...,
    832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1330352, ...}) = 0
mmap(NULL, 3437208, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) 
    = 0x7f47dac24000
mprotect(0x7f47dad63000, 2093056, PROT_NONE) = 0
mmap(0x7f47daf62000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE,
    3, 0x13e000) = 0x7f47daf62000
mmap(0x7f47daf67000, 17048, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, 
   -1, 0) = 0x7f47daf67000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 
    = 0x7f47db16e000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 
    = 0x7f47db16d000
arch_prctl(ARCH_SET_FS, 0x7f47db16d6f0) = 0
mprotect(0x7f47daf62000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7f47db186000, 4096, PROT_READ) = 0
munmap(0x7f47db16f000, 85050)           = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 
    = 0x7f47db183000
write(1, "You have been alive 10654 days\n"..., 31You have been alive 10654 days
) = 31
write(1, "You were born on 24/1/1980 which "..., 47You were born on 24/1/1980 
   which is a Thursday
) = 47
exit_group(0)

在这两个输出中,每个输出行对应于应用程序执行的一个函数调用,其中显示函数的参数和函数调用的返回值。与调试示例不同,列出的每个函数调用都是系统或系统库中的函数,因此表示调用的函数的更低层接口。例如,在应用程序中可能使用 C 或 C++ 中的 fpopen() 函数打开文件,但是这个函数实际上是更低层的 open() 函数的包装器。

了解应用程序正在执行的操作并不需要了解每个函数的情况。输出中的许多行与操作系统为装载和执行程序所做的初始化相关。这两个跟踪输出的基本结构是相同的:

  • 调用 execve() 函数以启动一个新程序。
  • 装载程序所需的库。在 Solaris 输出中,首先使用 resolvepath() 寻找库,然后使用 open() 打开库。对于 Linux,使用 stat() 检查库是否存在,然后使用 open() 打开它。
  • 为进程保留和分配一些内存。其中一部分内存是为应用程序保留的堆栈空间,一部分用来保存程序,其他内存保存程序使用的变量。
  • 最后,执行程序,调用 write() 函数输出年龄和生日信息。

如果执行跟踪并希望了解每个步骤的具体情况,可以使用 man 命令访问每个函数的手册页。

识别应用程序启动问题

在启动应用程序时的一个典型问题是,程序无法正确地初始化,但是在终止时给出一个不完整或导致误解的消息。对应用程序运行跟踪常常可以揭示这个问题的根源。例如,清单 5 显示一个测试应用程序运行失败了。

清单 5. 应用程序失败
$ ./errnoacc 
ERROR: Application failed to initialize

错误消息并没有提供关于应用程序为什么会启动失败的具体信息。在这里,问题是故意引入的,但是您使用的任何命令或应用程序都可能出现相同的问题,而错误消息没什么帮助,有时候甚至没有错误消息。

对应用程序运行跟踪可能会提供一些线索(见清单 6)。

清单 6. 运行跟踪
$ truss ./errnoacc
execve("errnoacc", 0x08047B20, 0x08047B28)  argc = 1
mmap(0x00000000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, -1, 0) 
    = 0xFEFB0000
resolvepath("/usr/lib/ld.so.1", "/lib/ld.so.1", 1023) = 12
getcwd("/export/home/mc", 1014)                 = 0
resolvepath("/export/home/mc/errnoacc", "/export/home/mc/errnoacc", 1023) = 24
xstat(2, "/export/home/mc/errnoacc", 0x080477E4) = 0
open("/var/ld/ld.config", O_RDONLY)             = 3
fxstat(2, 3, 0x080476C4)                        = 0
mmap(0x00000000, 144, PROT_READ, MAP_SHARED, 3, 0) = 0xFEFA0000
close(3)                                        = 0
sysconfig(_CONFIG_PAGESIZE)                     = 4096
xstat(2, "/usr/lib/libc.so.1", 0x08046F04)      = 0
resolvepath("/usr/lib/libc.so.1", "/lib/libc.so.1", 1023) = 14
open("/usr/lib/libc.so.1", O_RDONLY)            = 3
mmap(0x00010000, 32768, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_ALIGN, 3, 0) = 0xFEF90000
mmap(0x00010000, 1413120, PROT_NONE, MAP_PRIVATE|MAP_NORESERVE|MAP_ANON|MAP_ALIGN, 
    -1, 0) = 0xFEE30000
mmap(0xFEE30000, 1302809, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_TEXT, 3, 0) 
    = 0xFEE30000
mmap(0xFEF7F000, 30862, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|
    MAP_INITDATA, 3, 1306624) = 0xFEF7F000
mmap(0xFEF87000, 4776, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_ANON, 
    -1, 0) = 0xFEF87000
munmap(0xFEF6F000, 65536)                       = 0
memcntl(0xFEE30000, 187632, MC_ADVISE, MADV_WILLNEED, 0, 0) = 0
close(3)                                        = 0
mmap(0x00010000, 24576, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON|MAP_ALIGN,
    -1, 0) = 0xFEE20000
munmap(0xFEF90000, 32768)                       = 0
getcontext(0x08047534)
getrlimit(RLIMIT_STACK, 0x0804752C)             = 0
getpid()                                        = 15727 [15726]
lwp_private(0, 1, 0xFEE22A00)                   = 0x000001C3
setustack(0xFEE22A60)
sysi86(SI86FPSTART, 0xFEF879BC, 0x0000133F, 0x00001F80) = 0x00000001
open("/etc/shadow", O_RDONLY)                   Err#13 EACCES [file_dac_read]
ioctl(1, TCGETA, 0x08046BB0)                    = 0
fstat64(1, 0x08046B10)                          = 0
ERROR: Application failed to initialize
write(1, " E R R O R :   A p p l i".., 40)      = 40
_exit(0)

问题出现在这一行上:open("/etc/shadow", O_RDONLY) Err#13 EACCES [file_dac_read]

在这里,应用程序试图打开一个文件,但是因为有文件权限保护这个文件,运行应用程序的用户没有访问权。因为无法打开这个文件,应用程序终止并在进程中写错误消息。

跟踪正在运行的应用程序

希望跟踪应用程序常常是因为应用程序已经启动,而您希望查明应用程序为什么不工作了。与初始化示例一样,应用程序提供的错误消息或其他信息常常没有准确地指出问题。

试图锁定或访问另一个进程正在使用的资源会使应用程序看起来冻结了,没有响应。

strace 和 truss 都能够 “连接” 到正在运行的进程。连接到进程的跟踪与从命令行运行进程的跟踪相似,也产生程序正在执行的系统函数的列表。实际的跟踪从跟踪启动时执行的函数开始;对于在执行期间 “挂起” 的程序,跟踪应该会显示程序正在等待的函数。

要想跟踪正在运行的程序,需要指定要跟踪的进程的进程 ID (PID)。例如,在清单 6 中,跟踪的程序已经停止,但是没有报告错误。这里使用 ps 工具列出正在运行的进程(见清单 7)。

清单 7. 使用 ps 工具列出正在运行的进程
$ ps -ef|grep errlock
      mc 15779 15747   0 18:26:59 pts/2       0:00 ./errlock
      mc 15742   680   0 18:26:36 pts/3       0:00 ./errlock
      mc 15817 15784   0 18:28:44 pts/4       0:00 grep errlock
      mc 15734   680   0 18:25:00 pts/3       0:01 /usr/bin/emacs-nox errlock.c
$ truss -p 15779
fcntl(3, F_SETLKW, 0x08047AC4)  (sleeping...)

在输出中可以看到,已经调用了 fcntl() 函数,它要在一个文件上设置锁。在这里,这个函数会一直等待到成功地设置锁,然后才继续运行。不幸的是,另一个进程已经锁住了这个文件,所以第二个应用程序必须等待第一个应用程序使用完文件并释放锁。

对于这种情况,truss 有点儿局限性:它无法指出要锁住哪个文件,也无法指出当前锁住了哪个文件,从而阻碍了第二个程序的执行。这是因为跟踪过程是在已经调用了打开文件的函数之后启动的。truss 和 strace 只跟踪在它们执行期间调用的函数;它们无法查明已经调用的函数。

获取堆栈跟踪

可以看出,truss 对于监视整个程序很有用,但是对于监视已经启动的程序可能有点儿局限性。如果使用基于 SVR4 的 UNIX,比如 Solaris 或 AIX,那么 pstack 命令可能有帮助。

pstack 命令实际上属于一组进程检查命令,这些命令输出正在运行的进程的相关信息。其他工具包括 pfiles(输出进程使用的文件的列表)和 psig(显示信号和信号处理函数的列表)。

在使用这些命令时,需要指定进程的 PID。pstack 命令输出一个正在运行的进程的调用堆栈,显示在进程到达当前函数之前调用的函数。例如,对正在等待被锁住的文件的进程使用 pstack,会产生清单 8 中的输出。

清单 8. 对正在等待被锁住的文件的进程使用 pstack
$ pstack 15828
15828:  ./errlock
 feef0877 fcntl    (3, 7, 8047ac4)
 feedcd49 fcntl    (3, 7, 8047ac4, 8050e74) + 91
 08050f10 main     (1, 8047b24, 8047b2c) + d8
 08050cdc _start   (1, 8047c08, 0, 8047c12, 8047c7d, 8047c8e) + 80

在这里,它没有提供我们需要的信息。现在试试 pfiles(见清单 9)。

清单 9. 使用 pfiles
$ pfiles 15856
15856:  ./errlock
  Current rlimit: 256 file descriptors
   0: S_IFCHR mode:0620 dev:292,0 ino:989038936 uid:101 gid:7 rdev:24,3
      O_RDWR|O_NOCTTY|O_LARGEFILE
      /dev/pts/3
   1: S_IFCHR mode:0620 dev:292,0 ino:989038936 uid:101 gid:7 rdev:24,3
      O_RDWR|O_NOCTTY|O_LARGEFILE
      /dev/pts/3
   2: S_IFCHR mode:0620 dev:292,0 ino:989038936 uid:101 gid:7 rdev:24,3
      O_RDWR|O_NOCTTY|O_LARGEFILE
      /dev/pts/3
   3: S_IFREG mode:0666 dev:182,65545 ino:198 uid:101 gid:10 size:0
      O_RDWR
      advisory write lock set by process 15828
      /export/home/mc/lockdemo

这些输出就比较有用了。可以看到进程已经打开的文件称为 lockdemo ,因为 truss 显示正在等待文件锁,所以可能是这个文件导致了问题。

truss 和 strace 都是被动的跟踪。可以查看正在执行的函数,但是无法了解关于正在发生的情况的详细信息,也无法更有针对性地指定要跟踪的东西和跟踪时要输出的信息。


用 DTrace 进行动态跟踪

Solaris、FreeBSD 和 Mac OS X 内置的 Dynamic Tracing (DTrace) 功能提供一个更加动态的跟踪环境。与 truss 和相似的工具不同,可以使用 DTrace 检查正在运行的程序的内部情况,而不只是查看系统调用。另外,可以使用 DTrace 编写应用程序跟踪脚本,从而定制在跟踪过程中希望提取的信息。

DTrace 概述

DTrace 结合了您在 truss 和 strace 中已经看到的许多跟踪原理,但是在跟踪应用程序所用的方法和机制方面更加灵活。

truss 和 strace 只列出内核空间中的函数,而 DTrace 可以显示应用程序的函数名、它所依赖的任何库以及调用的内核函数。这把跟踪的方便性提高到与调试器差不多的水平。但是,与调试器不同,不能修改值,也不能暂停或以其他方式改变应用程序的执行过程。只能跟踪执行过程,而不能控制它。

与大多数跟踪工具和调试器相比,DTrace 的另一个独特之处是可以编写跟踪脚本,跟踪脚本定义要跟踪应用程序中的哪些方面,以及在执行应用程序时应该报告哪些信息。例如,可以指定 DTrace 只报告关于某个函数的信息,让它只输出函数调用中的一个参数。

除了输出特定信息之外,DTrace 还为许多实用程序值和函数提供内置支持。例如,在脚本中可以记录调用函数时的时间戳,然后把这个时间戳值与函数完成时的时间戳进行比较。通过比较这两个值,可以得到特定函数或操作的执行时间,可以使用这种跟踪信息提供执行和性能统计数据。

探测和提供者

DTrace 会在应用程序中添加检测机制,从而识别不同的执行点。这些执行点称为探测(probe),包括在内核、库和程序中定义的探测。内核、库和用户程序中的所有函数都可以指定为探测。另外,可以使用静态定义的探测识别感兴趣的特殊执行点。例如,在内核中可以使用探测识别向磁盘写数据的执行点。开发人员可以在程序中添加特定的探测,从而允许用户启用跟踪。这些探测称为 User-land Statically Defined Tracing(USDT)。

指定探测所用的结构是 provider:module:function:name,其中的 provider 是提供者的名称(例如,程序名、内核或操作系统的特定部分),module 是内核模块或库,function 常常是模块或程序中的函数名,name 标识探测。提供者还有一个特殊的标识符,PID;这用来标识正在运行的任何程序,可以用它跟踪正在运行的程序中的任何函数。

名称常常是系统或程序中已经定义的探测的名称。DTrace 还支持函数边界跟踪 (FBT),这可以跟踪进入和退出内核、库或应用程序中任何函数的执行点。当调用函数时,触发进入探测;当函数返回或完成执行时,触发返回探测。

指定探测的结构中的任何部分都可以忽略(在这种情况下,与所有项目匹配),也可以使用通配符。例如,可以使用 provider::: 指定某个提供者中的所有探测。还可以只指定一个 PID 提供者中的进入探测:pid$target:::entry

上面指定的探测与 truss 的操作相似,但是涉及的函数包括程序、操作系统和程序依赖的任何库。这提供比 truss 更广的范围。

在使用 PID 提供者时,可以使用模块标识程序。如果不指定模块,就会跟踪程序调用的每个函数。通过指定程序名,可以把输出限制在程序内定义的函数。例如,可以使用 pid$target:ageindays:: 跟踪 ageindays 应用程序中的函数的执行情况。

可以使用 dtrace 跟踪任何程序;不需要以特殊方式编译程序。因此,与 truss 不同,即使您不掌握程序的源代码,也可以深入了解应用程序。

可以使用 dtrace 工具获得探测列表;-l 命令行选项列出系统中定义的所有探测:$ dtrace -l

这个列表可能非常长。

单行跟踪

正如前面提到的,可以使用 DTrace 跟踪任何应用程序和其中的任何函数。从命令行使用 dtrace 有三种方式:指定一个命令;指定一个进程 ID;指定一个命名的静态探测。

在命令行上指定要执行的命令(使用 -c),使用 -n 选项指定希望监视的探测(见清单 10)。

清单 10. 指定希望监视的探测
$ dtrace -n 'pid$target:ageindays::entry' -c './ageindays 24/1/1980 26/3/1980'
dtrace: description 'pid$target:ageindays::entry' matched 7 probes
You have been alive 62 days
You were born on 24/1/1980 which is a Thursday
dtrace: pid 15925 has exited
CPU     ID                    FUNCTION:NAME
  1  57147                     _start:entry 
  1  57148                      __fsr:entry 
  1  57153                       main:entry 
  1  57152                  check_day:entry 
  1  57152                  check_day:entry 
  1  57151                  calc_diff:entry 
  1  57150                  leap_year:entry 
...

现在,指定一个正在运行的程序的进程 ID 以及希望跟踪的探测。例如,清单 11 跟踪 inetd 守护进程对 syslog 系统的调用。

清单 11. 跟踪对 syslog 系统的调用
$ dtrace -n 'pid$target::syslog:entry { printf("%d %s", arg0, copyinstr(arg1)) }' 
   -p `pgrep -x inetd`

还可以指定静态探测的名称,这与正在运行的任何进程中的所有探测匹配。例如,清单 12 跟踪对任何 exec 函数的调用(例如,当用户在 shell 中运行命令时)。因为没有指定特定的进程,这个探测会跟踪系统上任何用户对此函数的任何调用(见清单 12)。

清单 12. 指定静态探测的名称
$ dtrace -n 'syscall::exec*:entry'
dtrace: description 'syscall::exec*:entry' matched 2 probes
CPU     ID                    FUNCTION:NAME
  0  56750                      exece:entry 
  0  56750                      exece:entry 
  0  56750                      exece:entry

在上面这些示例中,只使用了基本输出,输出的信息只包括函数或探测名称以及触发探测的 CPU 和进程 ID。可以通过使用 DTrace 脚本编程语言产生更丰富、更有选择性的探测输出。

编写 DTrace 脚本

DTrace 脚本编程语言提供一种简单的编程机制,可以在提供者触发探测时执行特定的操作(称为动作)。这种语言不具备 PHP 或 Perl 等完整语言环境的灵活性,但是可以使用它在变量中记录信息、执行基本的计算和支持基本的决策。

下面的基本示例在执行特定的探测点时输出值。可以使用别名 arg0、arg1 等获取探测或函数的参数。这个示例监视给定的进程中以 open 开头的所有函数,显示每个函数打开的文件。

清单 13. 监视给定的进程中以 open 开头的所有函数
#!/bin/dtrace -s

#pragma D option quiet

pid$target::open*:entry
{
   printf("Opened: %s\n",copyinstr(arg0));
}

可以把这段文本保存在 open.t 文件中,然后在此文件上设置执行位:$ chmod +x open.t

这个文件现在是一个可执行的脚本。在使用它时,应该通过 -p 命令行选项指定进程 ID。例如,对一个 shell 运行它,就会获得 shell 在执行期间打开的文件的列表(见清单 14)。

清单 14. 使用 -p 选项
$ ./open.t -p 15930
Opened: /root/.bash_path
Opened: /root/.bash_vars
Opened: /root/.bashrc
Opened: /root/.bash_aliases
...

DTrace 脚本的一种典型用途是合并和汇总信息,提供不同操作的计数和时间差。DTrace 脚本中的探测在一个线程中依次执行,这意味着可以监视一系列探测的执行情况。许多探测是成对提供的,启始探测表示操作开始的位置,结束探测表示操作完成的位置。

通过记录触发启始探测和结束探测的时间,就可以判断出操作花费的时间。在 DTrace 脚本中,可以使用 self 变量保存启始时间。例如,清单 15 中的脚本使用 MySQL 数据库系统中的命名探测监视查询的执行时间。

清单 15. 使用 MySQL 中的命名探测监视查询的执行时间
#!/usr/sbin/dtrace -s

#pragma D option quiet

dtrace:::BEGIN
{
   printf("%-80s %6s\n", "Query", "Duration (ms)");
}

mysql*:::query-start
{
   self->query = copyinstr(arg5);
   self->querystart = timestamp;
}

mysql*:::query-done
{
   this->elapsed = (timestamp - self->querystart) /1000000;
   printf("%-80s %6d\n", self->query, this->elapsed);
}

这个跟踪显示典型 DTrace 脚本的许多元素。首先,BEGIN 块输出一些标题信息,这在输出表格式数据时很有用。

query-start 探测包含许多参数,第六个参数 (arg5) 包含完整的查询文本。

在运行 MySQL 的服务器上执行查询时,可以获得执行每个查询花费的时间的统计数据(见清单 16)。

清单 16. 获得执行每个查询花费的时间的统计数据
$ ./basic.d
Query                                                           Duration (ms)
show tables                                                     203
select * from t1 where i <5                                  131526

这些示例只涉及 DTrace 的基本功能。更多示例和信息见 参考资料


跟踪网络数据包

跟踪应用程序代码可以揭示应用程序中正在发生的情况,能够查明系统正在执行的操作;但是,如果希望监视网络应用程序,希望了解正在通过网络传输哪些信息,那么该怎么办呢?有许多网络跟踪工具,Solaris 上的 snoop 工具、AIX 上的 iptrace 和适用于许多平台的 Ethereal 工具都可以查看网络上正在传输的数据包。

网络跟踪基础

跟踪网络数据包的几乎所有工具都采用相同的基本工作方式。一般情况下,计算机从网络读取数据包,但是只处理专门发送给这台计算机的网络数据包。在老式的以太网上,所有数据包发送给所有计算机。在使用网络交换机的情况下,如果希望查看发送给其他计算机的数据包,可能需要连接网络上的管理端口。

使用这些工具的方法和它们能够提供的信息有所差异,但是基本上是相同的。在 AIX 上,iptrace 是一个后台守护进程,所以必须通过显式地启动和停止工具来打开和关闭跟踪过程。启动它的命令是 # startsrc -s iptrace -a "-i tr0 /home/user/iptrace/log1"。停止它的命令是 # stopsrc -s iptrace

在 Solaris 上,snoop 工具是一个根据需要执行的应用程序:# snoop

但是,典型网络上的数据包数量非常大,可能很难动态地解码和显示它们。在这种情况下,可以通过设置端口名称或端口号、计算机名称或编号指定有限的扫描,也可以把原始数据输出到文件中,然后进一步处理这个文件。可以通过设置输出文件记录信息:$ snoop -o networkdump.log

输入 $ snoop -i networkdump.log 读取保存的数据。

在使用 snoop 跟踪应用程序时,可能希望进行更有针对性的搜索。

用 snoop 扫描特定的主机

在跟踪期间,使用 snoop 的典型目的是了解应用程序是否正在通过网络进行通信,或者了解正在交换的具体信息。

例如,如果要诊断在 NFS 服务器和 NFS 客户机之间发生的问题,可能希望确认这两台计算机确实正在交换信息。一种方法是使用 snoop 监视交换的数据。在客户机上,可以使用 snoop 记录与服务器交换的数据。

例如,清单 17 查看主机 bear 的通信流。

清单 17. 查看主机 bear 的通信流
$ snoop host bear    
Using device rge0 (promiscuous mode)
tweedledee.mcslp.pri -> bear.mcslp.pri TCP D=2049 S=1014 Syn Seq=1160567073 
    Len=0 Win=49640 Options=<mss 1460,nop,wscale 0,nop,nop,sackOK>
bear.mcslp.pri -> tweedledee.mcslp.pri TCP D=1014 S=2049 Syn Ack=1160567074 
    Seq=498630824 Len=0 Win=5840 Options=<mss 1460,nop,nop,sackOK,nop,wscale 7>
tweedledee.mcslp.pri -> bear.mcslp.pri TCP D=2049 S=1014 Ack=498630825 
    Seq=1160567074 Len=0 Win=49640
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C ACCESS3 FH=EC09 (read,lookup,
    modify,extend,delete)
bear.mcslp.pri -> tweedledee.mcslp.pri TCP D=1014 S=2049 Ack=1160567230 
    Seq=498630825 Len=0 Win=54
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R ACCESS3 OK (read,lookup,modify,
    extend,delete)
tweedledee.mcslp.pri -> bear.mcslp.pri TCP D=2049 S=1014 Ack=498630949 
    Seq=1160567230 Len=0 Win=49640
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=EC09
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri TCP D=2049 S=1014 Ack=498631065 
    Seq=1160567382 Len=0 Win=49640
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=02E4
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=8F7F
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=2764
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=FD7F
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=FF7F
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=8F7F
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=725E
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=8D7F
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=0C64
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=6CEC
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri NFS C GETATTR3 FH=5997
bear.mcslp.pri -> tweedledee.mcslp.pri NFS R GETATTR3 OK
tweedledee.mcslp.pri -> bear.mcslp.pri TCP D=2049 S=1014 Ack=498632341 
    Seq=1160569142 Len=0 Win=49640

可以看到正在交换数据。snoop 能够解码原始数据包的内容,从而判断实际交换的数据。在这里,NFS 使用 GETATTR3 网络数据包,它们提供文件的属性数据(比如名称、大小和所有者信息)。

这里显示了许多额外信息,所以还可以更明确地指定只选择 NFS 数据包。可以通过组合使用表达式指定要选择的数据,比如:$ snoop host bear and protocol nfs

要想获得更详细的信息,可以用详细模式输出交换的实际数据。如果启用详细模式,snoop 会显示数据包的所有成分,包括原始以太网头数据以及 TCP 和协议信息。清单 18 给出在通过 NFS 请求文件系统状态数据时发送的一个数据包。

清单 18. 通过 NFS 请求文件系统状态数据时发送的一个数据包
ETHER:  ----- Ether Header -----
ETHER:  
ETHER:  Packet 2 arrived at 16:09:4.99083
ETHER:  Packet size = 142 bytes
ETHER:  Destination = 0:1a:ee:1:1:c0, 
ETHER:  Source      = 0:1d:60:1b:9a:2d, 
ETHER:  Ethertype = 0800 (IP)
ETHER:  
IP:   ----- IP Header -----
IP:   
IP:   Version = 4
IP:   Header length = 20 bytes
IP:   Type of service = 0x00
IP:         xxx. .... = 0 (precedence)
IP:         ...0 .... = normal delay
IP:         .... 0... = normal throughput
IP:         .... .0.. = normal reliability
IP:         .... ..0. = not ECN capable transport
IP:         .... ...0 = no ECN congestion experienced
IP:   Total length = 128 bytes
IP:   Identification = 61800
IP:   Flags = 0x4
IP:         .1.. .... = do not fragment
IP:         ..0. .... = last fragment
IP:   Fragment offset = 0 bytes
IP:   Time to live = 64 seconds/hops
IP:   Protocol = 6 (TCP)
IP:   Header checksum = c7b7
IP:   Source address = 192.168.0.2, bear.mcslp.pri
IP:   Destination address = 192.168.0.5, tweedledee.mcslp.pri
IP:   No options
IP:   
TCP:  ----- TCP Header -----
TCP:  
TCP:  Source port = 2049
TCP:  Destination port = 1013 (Sun RPC)
TCP:  Sequence number = 2161119694
TCP:  Acknowledgement number = 1253508400
TCP:  Data offset = 20 bytes
TCP:  Flags = 0x18
TCP:        0... .... = No ECN congestion window reduced
TCP:        .0.. .... = No ECN echo
TCP:        ..0. .... = No urgent pointer
TCP:        ...1 .... = Acknowledgement
TCP:        .... 1... = Push
TCP:        .... .0.. = No reset
TCP:        .... ..0. = No Syn
TCP:        .... ...0 = No Fin
TCP:  Window = 348
TCP:  Checksum = 0xec08
TCP:  Urgent pointer = 0
TCP:  No options
TCP:  
RPC:  ----- SUN RPC Header -----
RPC:  
RPC:  Record Mark: last fragment, length = 84
RPC:  Transaction id = 485864481
RPC:  Type = 1 (Reply)
RPC:  This is a reply to frame 1
RPC:  Status = 0 (Accepted)
RPC:  Verifier   : Flavor = 0 (None), len = 0 bytes
RPC:  Accept status = 0 (Success)
RPC:  
NFS:  ----- Sun NFS -----
NFS:  
NFS:  Proc = 18 (Get filesystem statistics)
NFS:  Status = 0 (OK)
NFS:  Post-operation attributes:  (not available)
NFS:  Total space = 488217268224 bytes
NFS:  Available space = 137675571200 bytes
NFS:  Available space - this user = 112875532288 bytes
NFS:  Total file slots = 60555264
NFS:  Available file slots = 58563011
NFS:  Available file slots - this user = 58563011
NFS:  Invariant time = 0 sec
NFS:

查看特定的信息也有助于判断不同的应用程序正在执行的操作。例如,可能希望监视正在从某个主机访问哪些网站,那么可以搜索 HTTP 协议,只在 snoop 输出中输出 HTTP 行(见清单 19)。

清单 19. 监视正在从某个主机访问哪些网站
$ snoop -v port 80 |egrep '^HTTP'
...
HTTP: ----- HyperText Transfer Protocol -----
HTTP: 
HTTP: GET / HTTP/1.0
HTTP: User-Agent: Wget/1.10.2
HTTP: Accept: */*
HTTP: Host: www.bbc.co.uk
HTTP: Connection: Keep-Alive
HTTP: [...]
...

egrep 只选择以 HTTP 开头的行。在这里可以看出有人访问了 BBC 网站。


结束语

结束语

在许多不同的情况下,需要跟踪应用程序。对于开发人员来说,可以通过跟踪应用程序来诊断问题,这可能比使用调试器更方便。DTrace 等工具的跟踪能力更强,可以获得非常有针对性的丰富的应用程序信息。

对于管理员来说,跟踪是寻找诊断问题所需的信息的惟一方法。在无法访问源代码的情况下,跟踪常常是判断应用程序正在执行的操作的惟一方法。本教程讲解了如何使用跟踪便捷地寻找和诊断问题。


下载

描述名字大小
ageindays 应用程序ageindays.zip5KB

参考资料

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=393897
ArticleTitle=跟踪 UNIX 应用程序的解决方案
publish-date=06032009