这篇还是关于DTrace的,是一个具体的实例,用于检测一段C++代码的内存泄漏,看了以后我觉得非常好,所以收录在这里。
原作者是作者:李凌云和张一峰(laoeyu),文章转自:http://dev.csdn.net/author/laoeyu/4c53f2acbda94645ad90c0f117b6afbe.html
DTrace可以帮助我们解决Memory Leak问题。下面给出一个内存泄漏的例子,看一下如何通过DTrace找到这个Memory Leak。
#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()那 个无参数的构造函数,于是我们就找到了哪部分代码导致的内存泄漏。
看上这篇文章以后,我就赶紧跑到Solaris下实践了,发现在Solaris10这个版本中,上面的trace.d脚本不能用libc这个provider,直接写成pid$1::malloc:entry这样就可以。另外在编译C/C++程序的时候,需要加上mem库,即cc -lumem xxx.c,然后再用ps -all查看进程号,最后再用dtrace -s ./trace.d pid这样的形式启动dtrace,之后的内容就如上所示了。