前言
C/C++运行效率高,操作系统内核和有性要求的程序(如游戏引擎)都要求用C/C++编写。其实C/C++强大的一点就是可以使用指针自由控制内存的使用,及时申请内存和释放内存,让其他编程语言高效运行。然而,内存管理是一把双刃剑。用的好,手臂会断的。如果应用堆上的内存没有得到及时有效的释放,就会造成内存泄漏。随着时间的推移,程序将耗尽系统内存,导致系统运行问题。就像你每天去图书馆借十几本书,直到图书馆关门才还。C语言中申请内存和释放内存的方法是malloc和free。C++与C兼容,所以可以使用malloc和free,而在面向对象的情况下可以使用new和delete,可以自动执行构造函数和析构函数。
如何发现内存泄漏
内存泄漏一般不会造成程序崩溃,所以比较隐晦,但是查找内存泄漏的方法也很简单,就是让程序运行一段时间,然后依次检查内存变化,通过Task Manager (windows)或者top(unix/linux)监控某个进程的内存变化更方便。有些程序内存泄漏相对较小,但找到它们的内存泄漏只是时间问题。这是一个有内存泄漏的程序的内存变化时间图。可见其内存占用总体上是在增加的
在内存泄漏较大的情况下,机器的cpu利用率飙升,cpu的等待百分比增加。从上面可以看到,交换内存的使用在不断增加,kswap进程不时出现在进程列表中。在linux中,watch-n1 "PS-o vsz-p
Valgrind定位内存泄漏
Valgrind是用于构建动态分析工具的工具框架。Valgrind工具可以自动检测很多内存管理和线程错误,并对程序进行详细分析。也可以使用Valgrind构建新工具。Valgrind的默认工具是内存检测,除此之外还有其他监控功能,这里就不讨论了。
valgrind的安装
linux下debian/ubuntu派系的安装方法:
itcast@ubuntu:~$ sudo apt install valgrindredhat/centos下[itcast@localhost ~]$ sudo yum install valgrindvalgrind的使用
下面是一个测试用C程序的例子
#include <。stdlib.h>。
#include <。stdio.h>。
void func()
{
//只申请内存不释放
void * p = malloc(sizeof(int));
}
int main()
{
func();
getchar();
返回0;
}
编译程序
gcc -o ./a.out ./main.c
使用valgrind命令执行程序,并将日志输出到文件中
val grind-log-file = ValRePOrt-leak-check = full-show-可达=yes - leak-resolution=low。/a.out
–log-log-file = val report指定在当前执行目录中生成分析日志文件,文件名为val report–leak-check = full以显示每个泄漏的详细信息–show-可达= yes是否检测控制范围之外的泄漏,如全局指针和静态指针,并显示所有内存泄漏类型–leak-resolution =低内存泄漏报告整合级别
最终执行输出的内容在下面的报告中解释,其中==98725==指的是流程编号。如果程序以多进程方式执行,将显示多个进程的内容。第一段是valgrind的基本信息。
==97825== Memcheck, a memory error detector==97825== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.==97825== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info==97825== Command: ./a.out==97825== Parent PID: 97615==97825==第二段是堆内存分配的总结信息,提到程序一共申请了三次内存,其中两次被释放,分配了2052字节
==97825====97825== HEAP SUMMARY:==97825== in use at exit: 4 bytes in 1 blocks==97825== total heap usage: 3 allocs, 2 frees, 2,052 bytes allocated==97825==第三段的内容描述了内存泄漏的具体信息,其中一个内存块占用4个字节。当调用malloc时,我们可以看到func函数最终在调用堆栈中调用了malloc,所以这些信息准确地定位了我们泄漏的内存被应用的位置。
==97825== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1==97825== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)==97825== by 0x4006C7: func (in /home/itcast/workspace/memleak/a.out)==97825== by 0x4007E8: main (in /home/itcast/workspace/memleak/a.out)最后一段是总结,4字节是内存泄漏
==97825====97825== LEAK SUMMARY:==97825== definitely lost: 4 bytes in 1 blocks==97825== indirectly lost: 0 bytes in 0 blocks==97825== possibly lost: 0 bytes in 0 blocks==97825== still reachable: 0 bytes in 0 blocks==97825== suppressed: 0 bytes in 0 blocks==97825====97825== For counts of detected and suppressed errors, rerun with: -v==97825== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)Mallinfo手动打印内存信息定位
内存泄漏的位置基本上可以通过valgrind的日志来定位,在valgrind的日志中可以清楚的看到new和delete或者malloc和free不能一一对应。第二种方式是通过日志来观察,每次调用可疑接口后,可以调用mallinfo函数来打印当前进程占用的内存量。如果通过日志文件发现当前进程的内存使用不断增加,则可以认为可疑接口是内存泄漏的罪魁祸首。这样可以不断缩小怀疑的范围,直到最终定位到泄露的代码。对上述程序进行必要的调整
#include <。stdlib.h>。
#include <。stdio.h>。
#include <。malloc.h>。
void func()
{
printf(" func n ");
void * p = malloc(sizeof(int));
//免费(p);
}
void displayMallInfo()
{
struct mall info = mall info();
printf(" = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = n ");
printf("arena:%dn ",info . arena);
printf("ordblks:%dn ",info . ordblks);
printf("smblks:%dn ",info . SMB lks);
printf("hblks:%dn ",info . hbl ks);
printf("hblkhd:%dn ",info . hblkhd);
printf("usmblks:%dn ",info . usmblks);
printf("uordblks:%dn ",info . uordblks);
printf("fordblks:%dn ",info . Ford blks);
printf("keepcost:%dn ",info . keep cost);
printf(" = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = n ");
}
int main()
{
displayMallInfo();
func();
displayMallInfo();
getchar();
返回0;
}
通过重新编译正在运行的程序,您可以看到以下信息:
itcast @ Ubuntu:~/workspace/mem leak $。/a.out
===========================
竞技场:0
ordblks:1
smblks:0
hblks:0
hblkhd:0
usmblks:0
uordblks:0
for blks:0
keepcost:0
===========================
功能
===========================
竞技场:135168
ordblks:1
smblks:0
hblks:0
hblkhd:0
usmblks:0
uordblks:1072
福特布鲁克:134096
keepcost:134096
===========================
要用这种方法总结某个函数的内存分配,需要熟悉linux中虚拟内存分配的原理。malloc在linux中的实现还调用了系统接口brk、sbrk、mmap等。linux实现内存应用解释:arena使用sbrk分配malloc(单位字节)内存总大小,普通(即非fastbin)空空闲块的ordblks数。smblks fastbin空闲块的数量(参见mallopt(3))。(不使用字典)hblks当前使用mmap(2)分配的块数。(见mallopt(3)中关于M_MMAP_THRESHOLD的讨论)。)hblkhd当前使用mmap(2)分配的块中的字节数。usmblks在空之间分配“高水位线”,即最大值(此字段未使用,始终为0),分配量在空之间。此字段仅在非线程环境中维护。fsmblks fastbin空空闲块中的总字节数。(未使用字典)为使用uordblks而分配的总字节数。fordblks 空空闲块中的总字节数。
监控与系统内存相关的调用
一般我们的操作环境中没有安装valgrind,所以这种情况valgrind无法跟踪,所以只能通过日志来跟踪这种情况。如果日志打印不出来,可以用另一种更简单的方式来进行:直接在strace中进行内存泄漏的过程。
strace -p <。pid>。
根据strace的内容,在内存大量泄漏的进程中,有很多系统调用申请系统内存,如下所示:
itcast @ Ubuntu:~/workspace/mem leak $ sudo strace-p 97495
itcast的[sudo]密码:
附工艺97495
restart _ syscall(& lt;...恢复中断的纳米睡眠...>。) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa933374000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa933273000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa933172000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa933071000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa932f70000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa932e6f000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa932d6e000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa932c6d000
nano LEEP({ 1,0},0x7fff275b1160) = 0
mmap(NULL,1052672,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7fa932b6c000
nano LEEP({ 1,0},{0,297924314}) =?ERESTART_RESTARTBLOCK(被信号中断)
- SIGINT {si_signo=SIGINT,si_code=SI_KERNEL} -
++被SIGINT ++杀死
可以看出,每次程序调用malloc,系统的api实际上都是调用mmap来申请虚拟内存。这里根据系统调用brk前后的几次系统调用,大致确定应用内存在代码中的大概位置,然后通过检查代码就可以确定泄漏。
重写malloc以实现自定义跟踪
如果能给自己程序的每个malloc和free call添加一些自定义代码,配合一些跟踪记录的手段,也可以自己实现内存泄漏跟踪。以下是一个例子
#定义自由(p) {
printf(" # % s:% d:% s():free(0x % LX) n ",__FILE__,__LINE_,
__func__,(无符号长)p);
免费(p);
}
#定义malloc(大小)({
void *ptr=malloc(大小);
printf(" # % s:% d:% s():malloc(0x % LX) n ",__FILE__,__LINE_,
__FUNCTION__,(无符号长)ptr);
ptr
})
编译程序后,您可以看到以下日志输出
# main . c:19:func():malloc(0x1b 61420)
# main . c:20:func():free(0x1b 61420)
如果地址信息可以在每个malloc进程中记录在链表之类的容器中,那么每次记录都可以从空闲中移除,还没有释放的内存可以在最后的程序执行后打印出来。但是这种方法的局限性在于只能对自己编写的代码生效。如果调用第三方库,并且在第三方库中使用malloc和free,则此方法无法跟踪第三方库的内存泄漏。Malloc和free可以通过在C语言中定义宏来重新定义,new和delete运算符可以在C++中重载来实现相同的功能,这里就不多讨论了。
总结
上述方法适用于内存泄漏快,每次泄漏多的情况。如果内存一次只泄漏一点点,就很难跟踪,只能依靠valgrind工具辅助或者依靠代码读取进行故障排除。此外,STL导致的内存泄漏也是一个相对容易忽视的问题,也是最难排查的问题。之前我们用string.append()保存日志信息,不断追加,最后一步写到本地磁盘,释放请求的内存。但是由于没有创建本地磁盘的相关目录,写入失败,内存没有释放,导致内存泄漏,服务器停机。
1.《nanosleep valgrind排查内存泄露》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《nanosleep valgrind排查内存泄露》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/guonei/1118067.html