您的位置:首页技术文章
文章详情页

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

【字号: 日期:2024-06-08 09:48:15浏览:53作者:猪猪

本文示例源代码或素材下载

开始之前

本教程帮助 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 27unsigned 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 uh a v eb e e".., 31) = 31 You were born on 24/1/1980 which is a Thursday write(1, " Y o uw e r eb o r".., 47) = 47 _exit(134511508) 

在这两个输出中,每个输出行对应于应用程序执行的一个函数调用,其中显示函数的参数和函数调用的返回值。与调试示例不同,列出的每个函数调用都是系统或系统库中的函数,因此表示调用的函数的更低层接口。例如,在应用程序中可能使用 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 157470 18:26:59 pts/20:00 ./errlock  mc 157426800 18:26:36 pts/30:00 ./errlock  mc 15817 157840 18:28:44 pts/40:00 grep errlock  mc 157346800 18:25:00 pts/30: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 编写应用程序跟踪脚本,从而定制在跟踪过程中希望提取的信息。

标签: Unix系统