王朝网络
分享
 
 
 

解决Solaris应用程序开发内存泄漏问题 (2)

王朝other·作者佚名  2006-06-09
宽屏版  字体: |||超大  

作者:李凌云, 张一峰

DTrace

DTrace是一个动态监测工具,它是在Solaris 10系统中Sun公司推出 的一个全新工具。DTrace这个工具是一个内嵌在Solaris系统中的子系统,也就是说我们可以在生产 环境下直接使用。它带有30000多个监测点 Probe)。通过这些监测点,可以动态的搜集操作系统 和应用程序的运行的方式和状态,帮助我们迅速找到问题的关键原因。如果这些检测点没有打开,是绝 对不会给系统带来任何开销,并且即使打开监测点,系统开销也是微乎其微的。DTrace不 仅能够起到监测的作用,而且可以动态修改系统和程序的行为。所有 这些都有相应的安全考虑,只有具有指定权限(Privilege)才可以使用相应的功能。

DTrace也可以帮助我们解决Memory Leak问题。下面给出一个内存泄漏的例子,看一下如何通过DTrace找到这个Memory Leak。如果大家对DTrace不 熟悉的话,可以参考一篇DTrace入门的文章:http://blog.gceclub.sun.com.cn/index.php?op=ViewArticle&articleId=516&blogId=4

#include <iostream.h>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

class TestClass

{

public:

TestClass() {

cout << "no-argument" <<endl;

}

TestClass(const char *name) {

cout << "arg = " << name << endl;

}

};

int main(int argc, char **argv)

{

TestClass *t;

TestClass *tt;

while (1) {

t = new TestClass();

t = new TestClass("Hello.");

tt = new TestClass("Goodbye.");

delete(t);

delete(tt);

sleep(1);

}

}

我们看到这段代码定义了一个TestClass类, 这个类含有两个构造函数,一个构造函数无参数,另一个构造函数带有一个const char *类型的字符串参数。这段代码的main函数有 一个while无限循环,在这个循环中,程序创建(new)三个对象,但只释放(delete)了两个对 象,造成内存泄漏。

这段程序虽然使用new和delete操作符,但我们知道最后还是使用libc库中的malloc和free两个函数实现内存分配和释放的。对于用户进程,DTrace提供了 特别的Provider,叫做pid Provider进行探测。pid Provider提供的监测点可以通过function函 数名制定,比如malloc函数,它的入口监测点是pid$1:libc:malloc:entry,它的返回监测点是pid$1:libc:malloc:return, 其中pid表示pid Provider,$1表示所要监测的进程号,它的值来自命令行第一个参数。除了malloc函数外,realloc和calloc函数也可以分配内存。为此,我们准备了一段检 测Memory Leak的脚本trace.d:

#!/usr/sbin/dtrace -s

pid$1:libc:malloc:entry

{

self->trace = 1;

self->size = arg0;

}

pid$1:libc:malloc:return

/self->trace == 1/

{

printf("Ptr=0x%p Size=%d", arg1, self->size);

ustack();

self->trace = 0;

self->size = 0;

}

pid$1:libc:realloc:entry

{

self->trace = 1;

self->size = arg1;

self->oldptr = arg0;

}

pid$1:libc:realloc:return

/self->trace == 1/

{

printf("Ptr=0x%p Oldptr=0x%p Size=%d", arg1, self->oldptr, self->size);

ustack();

self->trace = 0;

self->size = 0;

}

pid$1:libc:calloc:entry

{

self->trace = 1;

self->size = arg1;

}

pid$1:libc:calloc:return

/self->trace == 1/

{

printf("Ptr=0x%p Size=%d", arg1, self->size);

ustack();

self->trace = 0;

self->size = 0;

}

pid$1:libc:free:entry

{

printf("Ptr=0x%p ", arg0);

}

当 进程调用malloc函数且刚进入malloc函数时,pid$1:libc:malloc:entry入口监测点就被触发,其中DTrace内建变量 arg0存放了malloc函数的第一个参数,即所要申请的内存字节数。它被保存在一个线程级的self->size变量中,供后面引用。pid$1:libc:malloc:return检测 点会在malloc函数返回时被触发,DTrace会执行printf函数和ustack 函数。printf函数将打印出arg1以及self->size。其中,arg1也是DTrace内建变量,pid$1:libc: malloc:return返回监测点的arg1表示malloc函数的返 回值,即所分配的内存起始地址,而先前保留在self->size变量中内存字节数将一起被打印。ustack函数会打印出用户堆栈。与此类似,脚本对realloc函数和calloc函数的调用进入和返回都设置了监测点。在脚本最后是针对free函数的监测点,pid$1:libc: free:entry会在free函数调 用入口时触发,执行printf函数,printf函数会打印出被释放的内存地址arg0,也就是先前通过malloc、realloc和calloc函数所分配的内存地址。

我们执行前面编译出来的C++程序,同 时用DTrace执行trace.d脚本,并指定待检测的进程的进程号(Process ID)。

# dtrace -s ./trace.d `pgrep a.out`

CPU ID FUNCTION:NAME

0 46868 malloc:return Ptr=0x28fd0 Size=8

libc.so.1`malloc+0x6c

libCrun.so.1`__1c2n6FI_pv_+0x28

a.out`main+0xc

a.out`_start+0x108

0 46868 malloc:return Ptr=0x28fc0 Size=7

libc.so.1`malloc+0x6c

libc.so.1`strdup+0xc

a.out`__1cJTestClass2t5B6M_v_+0x1c

a.out`main+0x1c

a.out`_start+0x108

0 46868 malloc:return Ptr=0x28f50 Size=8

libc.so.1`malloc+0x6c

libCrun.so.1`__1c2n6FI_pv_+0x28

a.out`main+0x50

a.out`_start+0x108

0 46868 malloc:return Ptr=0x28fe0 Size=7

libc.so.1`malloc+0x6c

libc.so.1`strdup+0xc

a.out`__1cJTestClass2t5B6Mpkc_v_+0x14

a.out`main+0x68

a.out`_start+0x108

0 46868 malloc:return Ptr=0x28ff0 Size=8

libc.so.1`malloc+0x6c

libCrun.so.1`__1c2n6FI_pv_+0x28

a.out`main+0x9c

a.out`_start+0x108

0 46868 malloc:return Ptr=0x41458 Size=9

libc.so.1`malloc+0x6c

libc.so.1`strdup+0xc

a.out`__1cJTestClass2t5B6Mpkc_v_+0x14

a.out`main+0xb4

a.out`_start+0x108

0 46873 free:entry Ptr=0x28fe0

0 46873 free:entry Ptr=0x28f50

0 46873 free:entry Ptr=0x41458

0 46873 free:entry Ptr=0x28ff0

....

^C

由于测试C++程序是一段while循环,我们只需要看一段循环就可以了,所以在 一个循环输出后通过Ctrl+C中止DTrace的检测。通过这段输出信 息我们不难发现程序总共调用了6次malloc和4次free,通过比对malloc和free所涉及的内存地址Ptr,不难发现只有Ptr=0x28fd0以及Ptr=0x28fc0这两块内存没有调用free释放。ustack()提供的调用栈揭示内存泄漏发生在a.out`main+0xc和a.out`__1cJTestClass2t5B6M_v_+0x1c两处,这是函 数名与地址偏移量组成的调用栈信息。其中由于C++编译器的mangling特性,修改了一些函数名字,造成无法识别,我们可以通 过C++编译器提供的工具nm, gc++filt把这些名字demangle过来,也可以使用Sun Studio自带的工具dem进行翻译,比如:

# /opt/SUNWspro/bin/dem __1c2n6FI_pv_

__1c2n6FI_pv_ == void*operator new(unsigned)

# /opt/SUNWspro/bin/dem __1cJTestClass2t5B6M_v_

__1cJTestClass2t5B6M_v_ == TestClass::TestClass()

我们可以看到问题是出在调用new TestClass()那 个无参数的构造函数,于是我们就找到了哪部分代码导致的内存泄漏。

使用上述方法查找内存泄漏点时最令人头疼的事情莫过于人工匹配malloc和free涉及的内存地址。最好有个工具可以自动去除无内存泄漏的调用点,只留下内存泄漏的信息。以下的Perl脚本可以帮助我们自动匹配内存地址,只留下内存泄漏相关的信息。

# cat ./findleaks.pl

#!/usr/bin/perl

# findleaks.pl

use Data::Dumper;

my %hash = ();

while (<>) {

if ((/malloc:return Ptr=([^ ]*) Size=(.*)/) ||

(/calloc:return Ptr=([^ ]*) Size=(.*)/)) {

$hash{$1} = { size => $2 };

while (<>) {

last if /^$/;

$hash{$1}->{stack} .= $_;

}

}

elsif (/free:entry Ptr=([^ ]*)/) {

if (exists $hash{$1} and $hash{$1}) {

$hash{$1} = '';

}

}

elsif (/realloc:entry Ptr=([^ ]*) Oldptr=([^ ]*) Size=(.*)/) {

if ($1 eq $2) {

if (exists $hash{$1} and $hash{$1}) {

$hash{$1} = { size => $3 };

$hash{$1}->{stack} = '';

while (<>) {

last if /^$/;

$hash{$1}->{stack} .= $_;

}

}

} else {

$hash{$1} = '';

$hash{$2}= { size => $3 };

$hash{$2}->{stack} = '';

while (<>) {

last if /^$/;

$hash{$2}->{stack} .= $_;

}

}

}

}

foreach my $key (keys %hash) {

next if not $hash{$key}->{size};

print "Ptr=$key Size=", $hash{$key}->{size}, "n";

print $hash{$key}->{stack}, "n---------n";

}

使用方法是将trace.d的输出重定向到一个临时文件,然后将该临时文件作为findleaks.pl的输入。findleaks.pl的输出即为与内存相关的调用。注意findleaks.pl应置为可执行。执行结果如下:

# dtrace -s ./trace.d `pgrep a.out` > ./tmpfile

# ./findleaks.pl ./tmpfile

Ptr=0x29090 Size=8

libc.so.1`malloc+0x6c

libCrun.so.1`__1c2n6FI_pv_+0x28

CCtest`main+0xc

CCtest`_start+0x108

---------

Ptr=0x29060 Size=7

libc.so.1`malloc+0x6c

libc.so.1`strdup+0xc

CCtest`__1cJTestClass2t5B6M_v_+0x1c

CCtest`main+0x1c

CCtest`_start+0x108

---------

Ptr=0x29130 Size=8

libc.so.1`malloc+0x6c

libCrun.so.1`__1c2n6FI_pv_+0x28

CCtest`main+0xc

CCtest`_start+0x108

---------

Ptr=0x29000 Size=7

libc.so.1`malloc+0x6c

libc.so.1`strdup+0xc

CCtest`__1cJTestClass2t5B6M_v_+0x1c

CCtest`main+0x1c

CCtest`_start+0x108

---------

...

libgc

Solaris系统为C/C++语言提供了垃圾收集库:libgc,这个库会自动负责内存管理包括分配和释放。 虽然它不能够找到哪段代码导致内存泄 漏,但是可以帮助我们彻底解决内存泄漏问题。这个库包含在Sun Studio,支持X86和SPARC。安装了Sun Studio后,库文件的位置在/opt/SUNWspro/lib/。

libgc对内存的管理是通过自有的一个高效内存分配器 (Memory Allocator),对所有堆(Heap)中分 配内存都会打上标志,如果发现这些打上标志的内存没有被指针使用,它就会取消标志,并释放。libgc会 自动定期扫描,决定什么时候回收这些取消标志的内存。如果遇到没有内存可分配的情况,它也会执行回收动作。

libgc使用方式有两种:

Deployment Mode

功能:可以自动解决内存泄漏。由于使用自有的内存分配器, 避免了内存碎片,同时内存分配的操作也会更快。

使用方法非常简单,不需要修改代码,只需在link库文件 的时候指定链接libgc。

Development Mode

功能:除了提供上面一种模式所有的功能外,还能够解决Premature Free的问题。

Premature Free是指当某块内存还在被别的指针使用或者引用的时候,提前释放某个指针指向的这块内存。这种Premature Free会导致程序崩溃。

如果需要解决这种问题我们可以使用Development Mode的libgc,这 种使用方式需要修改代码。libgc库提供了三个函数用于处理Premature Free问题。

void gcFixPrematureFrees(void)

该函数会让free函数失效,free函数不会释放任何内存。内存的释放会全交给libgc,libgc会自动判断,在这块内存没有别人使用或者引用 后,会自动释放。

void gcStopFixingPrematureFrees(void)

该函数会中止修复Premature free,是 gcFixPrematureFrees 的反操作。

void gcInitialize(void)

在程序启动时候,libgc库文件会自动调用gcInitialize函数。该函数通常是作为一个预 设接口,用来调用gcFixPrematureFrees 函数。

使用方法如下:

#include <gct.h>

void gcInitialize(void)

{

gcFixPrematureFrees();

}

这三个函数的定义都在gct.h文件 中。

总结

以上介绍的是在Solaris下多种解决内存泄漏方法,每种方法都有自 己的优缺点,适用于不同的场合。我们把这几种方法作了一个比较,供大家参考:

特征

libumem

libgc

dbx

DTrace

需要修改代码

否*

需要重新编译

否*

Solaris版本要求

Solaris 9 u3以上(含)

Solaris 2.6以上(含)

Solaris 8以上(含)

Solaris 10

支持UltraSPARC

支持X86/X64

发现/解决内 存泄漏

发现内存泄漏

解决内存泄漏

发现内存泄漏

发现内存泄漏

可解决内存碎 片

是否可用于生 产环境

否*

使用是否方便

参考资料

1. Identifying Memory Management Bugs Within Applications Using the libumem Library

2. Using DTrace to Profile and Debug A C++ Program

3. 使用dbx调试程序

4. Using the C/C++ Garbage Collection Library, libgc

5. Solaris Dynamic Tracing Guide

6. Sanjeev Bagewadi's Weblog

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
>>返回首页<<
推荐阅读
 
 
频道精选
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
© 2005- 王朝网络 版权所有