作者: 李凌云,张一峰
概述
内存泄漏是应用软件开发过程中经常会遇到的问题,应用长期内存泄漏会占用大量操作系统内存资源,直接导致应用程序运行不稳定,严重时甚至还会影响到操作系统的正常运行。为了找到应用程序内存泄漏点,许多开发人员不得不在上千行乃至几十万行源程序中加入更多的调试代码,试图从调试信息中找到内存泄漏的根源,但通常来讲这种方法是事倍功半的。幸运的是,Solaris平台提供了好几个实用的工具,能够辅助开发人员对内存泄漏根源进行定位。笔者参考了 Sun公司官方网站上相关的英文技术文档,并认为这些对于我们中国广大的Sun技术爱好者有很好的指导作用。笔者经过消化整理后写下此文希望与大家共享。在下面的章节 中将初步讲述如何在Solaris 10操作系统下利用这些工具查找用户程序的内存泄漏点。文章各节标题如下:
6. libgc
7. 总结
8. 参考资料
内存泄漏及危害
如果要给内存泄漏下个定义的话,它应该属于软件程序设计的一种缺陷,该缺陷直接导致了程序在运行过程中无法释放不再需要的内存空间,从而造成内存资源浪费。具体来说,当用户程序在运行过程中需要动态获得内存时,操作系统总是从堆(heap)上分配相应的空间给应用,分配的结果是将该堆内存的起始地址通过指针返回给应用。正常情况下,应用使用完这块内存后,应通过系统调用主动通知操作系统回收这些堆内存以便重用。但是,如果由于设计缺陷导致在某些情况下程序没有主动地通知到操作系统,而后应用又失去了对这块内存的引用时,则该堆内存块将成为既不受程序控制,又不能被系统回收重用的“孤儿”内存,这便是我们所指的内存泄漏。
造成内存泄漏的设计缺陷多种多样,下面例举了部分典型的内存泄漏设计缺陷,它们都是开发人员经常会犯的毛病。
例1:
void foo( )
{
char *str;
str = (char *) malloc(32);
strcpy(str, "hello world");
return;
/* str所指向的32个字节的内存没有被释放,当foo()返回时造成内存泄漏 */
}
例2:
void PrintCWD( )
{
printf("cwd = %sn", getcwd(NULL, MAXPATHLEN));
return;
/* 某些系统调用本身就会在堆上申请一块内存空间,然后将指针返回 */
/* 给调用者。比如,系统调用getcwd()就将当前的工作目录路径保存 */
/* 在一块堆内存上,然后返回给调用者。应用应该使用指针接收该类 */
/* 指针,并在用完后释放该内存。而本例中却没有将getcwd()返回 */
/* 的内存块释放,从而造成内存泄漏 */
}
例3:
void foo( )
{
char *string1 = malloc(100);
char *string2 = malloc(200);
scanf("%s", string2);
string1 = string2; /* string1原先指向的100个字节的内存没有被释放,*/
/* 而后又被指向string2所指的内存块,造成前面*/
/* 100个字节的内存泄漏 */
free(string2);
free(string1); /* 这个free()调用会失败,因为string1指向的内存地址 */
/* 与string2的相同,而那块内存已经被释放了 */
return 0;
}
例4:
int MyFunction(int nSize)
{
char* p= new char[nSize];
if ( !GetStringFrom( p, nSize ) )
return -1; /* 在异常情况下,p所指向的nSize个字节内存没有被 */
/* 释放,造成内存泄漏 */
/* 使用p所指的内存 */
…
delete p;
return 0;
}
一个原先运行正常的系统由于安装运行了用户应用程序后经常发生以下任意症状时,则应用程序有可能存在内存泄漏问题:
没有特殊原因,应用程序经长时间运行后所占虚拟内存总量仍在持续性地不断增长(即使增长过程十分缓慢)。通过使用prstat可以查询进程内存使用情况。
在swap设备配置和运行正常,且没有大文件占用/tmp目录的情况下,经常有进程报告“Out of Memory”错误。通过使用swap -s命令可以查询swap空间使用情况,以及使用df -k查询/tmp的使用情况。
一个有内存泄漏问题的应用程序经过长时间运行后,通常会逐渐占用大量操作系统内存。操作系统会因内存短缺而造成整体性能下降,严重时可以造成系统中其他真正需要内存的进程因得不到内存空间而无法正常运行,操作系统也会由于内存耗尽而变得不稳定。在下面的章节中,我们将介绍在Solaris 10平台上有哪些工具帮助我们分析内存泄漏问题。
dbx
Sun Studio是Sun公司推出的面向C、C++和Fortran语言编程的开发环境,目前最新版本是Sun Studio 11。它包括一个非常友好而专业的GUI集成开发环境,以及像dbx、Performance Analyzer等优秀的辅助工具。其中,dbx工具除了可以帮助开发人员进行源代码级别的跟踪调试以外,它还可以帮助开发人员查找和定位应用程序中内存泄漏的问题。令人兴奋的是,Sun Studio 11不像以前各版本那样需要购买License,它是免费下载和使用的(包括商用),有兴趣的话可以到http://developers.sun.com/prodtech/cc/downloads/index.jsp下载。
Runtime Checking
dbx中提供内存泄漏检查功能的模块被称为RTC,即Runtime Checking。它除了提供内存泄漏检查功能外,还可以进行内存访问检查和使用检查,对于发现程序中内存越界访问或者变量未初始化就访问等编程问题很有帮助。缺省情况下,dbx不启用内存泄漏检查功能,用户可以通过check -leaks命令启用它。check -leaks是一个反复切换开关,在已经启用内存泄漏检查功能的情况下再使用该命令可以关闭这个功能。使用内存泄漏检查功能不需要对程序进行重编译,它也支持在优化过的目标代码中查找内存泄漏。
使用RTC模块有一些前提要求,比如:
程序必须是由Sun提供的编译器所编译生成的;
程序使用动态库方式链接libc库;
内存是通过libc库的malloc()、free()、realloc()或其他基于这些调用的函数进行申请和管理的;
程序不能被完全strip,即符号表必须存在。strip -x命令仍可以接受。
RTC还有部分限制,比如:
只能在Solaris操作系统上使用;
在基于非UltraSPARC芯片的主机系统上使用时,程序的text段和数据data段不能超过8MB空间。
RTC对于内存泄漏分三种情况:
Memory Leak(mel),即进程中不存在任何一个指针指向某内存块,则该内存块为真正的内存泄漏块。
Address in Block(aib),即进程中不存在任何一个指针指向某内存块的启始位置,却存在指向该内存块中间某位置的指针。这是一个可疑的内存泄漏,即它很可能会演变为Memroy Leak,但也不排除程序设计者为了某种需要故意设计成这样的。
Address in Register(air),即进程代码段及数据段中没有任何一个指针指向该内存块,但在至少一个寄存器中存在相关的指针。这是一个可疑的内存泄漏。如果程序在编译时使用了优化选项,比如-O等,则编译器有可能只将内存指针保留于寄存器中,否则这会演变为真正的内存泄漏。
dbx在报告内存泄漏时会区分上述三种情况,对于可能的内存泄漏,开发人员需要自行判断是否为真正的内存泄漏。
用dbx查内存泄漏
使用dbx检查内存泄漏是所有工具中最方便的。如果程序在编译时使用了-g选项,dbx可以很方便地将内存泄漏点定位到源程序代码行。使用dbx检查内存泄漏的典型过程如下:
1. 使用dbx启动被跟踪的程序。
$ dbx ./a.out
2. 用check -leaks打开内存泄漏检查开关。
(dbx) check -leaks
leaks checking - ON
3. 运行程序直至结束。当程序运行结束时,dbx会给出类似以下的内存泄漏报告。
(dbx) run
Running: a.out
(process id 26767)
Reading rtcapihook.so
Reading libdl.so.1
Reading rtcaudit.so
Reading libmapmalloc.so.1
Reading libgen.so.1
Reading libm.so.2
Reading libc_psr.so.1
Reading rtcboot.so
Reading librtc.so
RTC: Enabling Error Checking...
RTC: Running program...
Checking for memory leaks...
Actual leaks report (actual leaks: 2 total size: 43 bytes)
Total Num of Leaked Allocation call stack
Size Blocks Block
Address
========== ====== =========== =======================================
32 1 0x21198 memory_leak < main
11 1 0x21210 address_in_register < main
Possible leaks report (possible leaks: 0 total size: 0 bytes)
execution completed, exit code is 0
(dbx)
例子中报告了两个内存泄漏,分别为从main()过程调用到memory_leak()过程时有1次32字节的内存泄漏,以及从main()过程调用到address_in_register()过程时有1 次11字节的内存泄漏。为了得到具体的源代码行号,可以在运行程序前使用以下的命令将内存泄漏报告模式改为verbose,然后重新运行程序。
(dbx) dbxenv rtc_mel_at_exit verbose
(dbx) run
Running: a.out
(process id 26768)
RTC: Enabling Error Checking...
RTC: Running program...
Checking for memory leaks...
Actual leaks report (actual leaks: 2 total size: 43 bytes)
Memory Leak (mel):
Found leaked block of size 32 bytes at address 0x21198
At time of allocation, the call stack was:
[1] memory_leak() at line 8 in "leak.c"
[2] main() at line 42 in "leak.c"
Memory Leak (mel):
Found leaked block of size 11 bytes at address 0x21210
At time of allocation, the call stack was:
[1] address_in_register() at line 35 in "leak.c"
[2] main() at line 44 in "leak.c"
Possible leaks report (possible leaks: 0 total size: 0 bytes)
execution completed, exit code is 0
(dbx)
例子中报告了内存泄漏点分别在源代码第8行和第35行。
如果要检查一个守护进程类型的服务程序是否发生内存泄漏,上述方法就不适用了,这是因为守护进程永远不会运行结束。对此,dbx提供了一个showleaks的命令可以让开发人员在任何时候查看进程内存泄漏情况。另外,守护进程一般会多次进行fork(),所以也不适合采用dbx直接进行启动。因此,对于守护进程类程序,开发人员可以通过以下方法启动,然后dbx动态挂接到已运行的进程上再进行内存泄漏检查。
1. 设定环境变量,预装librtc.so。
$ LD_AUDIT=/opt/SUNWspro/lib/rtcaudit.so; export LD_AUDIT
缺省情况下在程序启动时librtc.so不会预装入系统。这意味着即使后来dbx动态挂接上该进程后,也无法使用RTC功能。但开发人员可以通过设定LD_AUDIT环境变量,指定应用程序启动时系统预装入librtc.so。方法是:对于32位应用,将LD_AUDIT指向<Sun Studio 11安装目录>/lib/下的rtcaudit.so,对于SPARC 64位应用,须指向lib/v9下的rtcaudit.so,对于AMD 64位应用,则为lib/amd64/下的rtcaudit.so。
2. 启动守护程序,并得到进程号。注意,启动应用后应及时使用unset命令去除LD_AUDIT设置。后继命令不应使用LD_AUDIT。
$ ./mysvc
$ unset LD_AUDIT
$ pgrep mysvc
27020
3. 令dbx动态挂接上守护进程,关闭同步跟踪,并打开内存泄漏检查。可根据需要在程序合适的位置设好断点,然后继续执行程序或单步跟踪执行程序。在必要的时候利用showleaks检查内存泄漏情况。
$ dbx ./myapp 27020
Reading mysvc
Reading ld.so.1
Reading libc.so.1
Reading rtcaudit.so
Reading libmapmalloc.so.1
Reading libgen.so.1
Reading libdl.so.1
Reading libm.so.2
Reading rtcboot.so
Reading librtc.so
Attached to process 27020
stopped in _syscall6 at 0xfed3edf4
0xfed3edf4: _syscall6+0x0020: blu _cerror ! 0xfeca06a0
Current function is main
38 msize = msgrcv(msgid, &msg, sizeof(1024), 0, 0);
dbx: internal warning: set_error_limit called too early
(dbx) dbxenv mt_sync_tracking off
(dbx) check -leaks
leaks checking - ON
RTC: Enabling Error Checking...
RTC: Running program...
(dbx) stop at 38
(2) stop at "mysvc.c":38
(dbx) showleaks -v
...
(dbx) cont
...
(dbx) showleaks -a -v
...
其中,showleaks命令缺省只报告自上次报告内存泄漏后新发现的内存泄漏。如果使用-a选项,则showleaks报告所有内存泄漏。-v选项是指verbose模式,可以给出更详细的报告。跟踪完成后,可使用quit命令退出dbx。
libumem
libumem的由来是原自SunOS 5.4中的Kernel Slab Allocator。为了加速系统虚拟内存操作,Sun的工程师发明了Kernel Slab Allocator,后来也被推广到Linux操作系统。Kernel Slab Allocator通过一种object cacheing技术策略来实现高效内存的处理。在实际使用中,这种Kernel Slab Allocator内存分配器被证明非常高效,在多颗CPU上,扩展性(Scability)表现亦极佳。由于Kernel Slab Allocator是工作在kernel状态,所以相应地产生了一个在用户空间工作的内存分配器: libumem。自Solaris 9(update3)开始,Solaris就自带这个全新的内存分配器: libumem。
libumem不仅能够优化程序的内存分配,而且还提供内存分配调试,记录功能,配合mdb工具我们可以轻松观察程序内存的分配情况和内存泄漏。在使用libumem检测内存泄漏的问题的之前,我们必须了解一些在调试中libumem提供给我们信息的内存结构。
libumem工作内存结构
libumem也是使用Slab概念。Slab是Slab Allocator中一个基本内存单元:Slab是代表一个或者多个虚拟内存中的页(Page),它通常会被分割成为多个大小等同的Chunks,被成为Buffer。Buffer含有用户所使用的数据,还会有一些额外的信息,不过这个取决环境变量的设置。这些额外的信息对我们调试,检测内存泄漏非常有用。下面就是Buffer的一个基本结构:
Metadata Section
User Data Section
Redzone Section
Debug Metadata Section
指针 (4字节)
验证码 (4字节)
Buffer结构中第一个section是Metadata,主要提供内存分配的长度信息,我们这里不使用,在32位程序应用中它是8个字节。Metadata后面是存储用户数据的User data Section。接着是Redzone部分,Redzone也是8个字节。最后是Debug Metadata也是8个字节。其中前四个字节代表一个指针,指向一个umem_bufctl_audit结构,这个结构记录着内存分配时候的堆栈。该结构的定义可以在/usr/include/umem_impl.h找到。后面四个字节是校验位,可以用来和前面字节一起来判断这个buffer有没有被破坏。
libumem使用方法
1. 预加载(Preload) libumem
如果在程序中需要调试,寻找内存泄漏,需要预先加载(Preload) libumem,并且设置上环境变量:UMEM_DEBUG=default,UMEM_LOGGING=transaction,LD_PRELOAD=libumem.so.1 。
在csh中设置的例子
% (setenv UMEM_DEBUG default; setenv UMEM_LOGGING transaction; setenv LD_PRELOAD libumem.so.1; ./a.out)
在bash中设置的例子
bash-3.00$ UMEM_DEBUG=default; UMEM_LOGGING=transaction; LD_PRELOAD=libumem.so.1; ./a.out
2. 运行程序,在程序运行时使用gcore命令对目标程序的进程生成core文件。
bash-3.00$ gcore `pgrep a.out`
gcore: core.1478 dumped
3. 使用mdb命令倒入core文件。
bash-3.00$ mdb core.1478
::umem_status命令查看libumem的日志功能是否打开。
> ::umem_status
Status: ready and active
Concurrency: 1
Logs: transaction=64k
Message buffer:
::findleaks命令查看是否有内存泄漏。
> ::findleaks
CACHE LEAKED BUFCTL CALLER
0003d888 1 00050000 main+0xc
--------------------------------------------------------
Total 1 buffer, 24 bytes
内存泄漏地址$<bufctl_audit,该命令会将该地址的内容以umem_bufctl_audit的结构,并且会显示内存泄漏的时候的用户堆栈。
> 50000$<bufctl_audit
0x50000: next addr slab
0 49fc0 4bfb0
0x5000c: cache timestamp thread
3d888 23764722653000 1
0x5001c: lastlog contents stackdepth
2e000 0 5
libumem.so.1`umem_cache_alloc+0x13c
libumem.so.1`umem_alloc+0x44
libumem.so.1`malloc+0x2c
main+4
_start+0x108
::umalog命令查看每次内存分配的时间,地址,堆栈。
> ::umalog
T-0.000000000 addr=55fb8 umem_alloc_32
libumem.so.1`umem_cache_alloc+0x13c
libumem.so.1`umem_alloc+0x44
libumem.so.1`malloc+0x2c
main+0x18
_start+0x108
T-0.000457800 addr=49fc0 umem_alloc_24
libumem.so.1`umem_cache_alloc+0x13c
libumem.so.1`umem_alloc+0x44
libumem.so.1`malloc+0x2c
main+0xc
_start+0x108
内存地址::umem_verify可以查看内存是否被破坏,比如内存的越界操作。
> ::umem_verify
Cache Name Addr Cache Integrity
umem_magazine_1 3c008 clean
umem_magazine_3 3c1c8 clean
umem_magazine_7 3c388 clean
umem_magazine_15 3c548 clean
umem_magazine_31 3c708 clean
umem_magazine_47 3c8c8 clean
umem_magazine_63 3ca88 clean
umem_magazine_95 3cc48 clean
umem_magazine_143 3ce08 clean
umem_slab_cache 3cfc8 clean
umem_bufctl_cache 3d188 clean
umem_bufctl_audit_cache 3d348 clean
umem_alloc_8 3d508 clean
umem_alloc_16 3d6c8 clean
umem_alloc_24 3d888 clean
umem_alloc_32 3da48 clean
... snip ...
内存地址::umem_log可以按CPU,线程打印出内存分配记录。
> ::umem_log
CPU ADDR BUFADDR TIMESTAMP THREAD
0 0002e064 00055fb8 10475e3dd1c98 00000001
0 0002e000 00049fc0 10475e3d62050 00000001
0003483c 00000000 0 00000000
000348a0 00000000 0 00000000
00034904 00000000 0 00000000
... snip ...