快速定位内存泄漏的套路

在这里插入图片描述

背景

偶然间发现一个模块挂掉了,并且没有生成core文件。这就让我很奇怪,因为一般如果是段错误导致程序挂掉,是会生成core文件的(我已经开启了coredump ulimit -c unlimited)。通过dmesg查看内核日志,发现是由于OOM kill机制导致的。如图:
在这里插入图片描述
既然发现了问题就一定要解决。通过查阅资料以及分析log终于定位到了内存泄漏的代码部分。本章我会结合自己的理解,一步一步的带大家分析,希望能够帮助到大家。

什么是OOM kill 机制?

简单的说就是当你的内存不足时,linux 内核为了不影响所有进程的正常使用,会启动该机制。首先会依据一些条件(进程内存占用大小,进程运行的时间等,一般都是那些内存占用比较多的进程)选出bad process。将其kill,释放它占用的内存。

暴力分析法

如果对于整体代码比较熟悉时,出现了内存泄漏,我们是可以估摸出大概位置的。比如:昨天还没有内存泄漏,今天就有了。那么这个bug肯定是某某在今天commit的。只要查看一下log就能知道大概位置。之后通过注释法(依次注释接口),也能够很快的定位到问题代码行。

valgrind 工具

如果对代码不是很熟悉(我就是这种情况,代码是外包人员写的,现在交付不管了),那我们最好的方式就是引用一些工具了。 网上推荐的工具有很多,我在这里使用的是valgrind。该工具的功能强大:

  1. memcheck :检测程序中的内存问题,如内存泄漏,越界,非法指针等
  2. callgrind:检测程序代码的运行时间和调用过程,以及分析程序性能。
  3. cachegrind:分析CPU的cache命中率,丢失率,用于进行代码优化。
  4. helgrind:用于检测多线程程序中出现的竞争问题
  5. Massif:堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理和栈的大小。

本篇主要从内存问题分析介绍,有时间我再研究一下其它功能。
案例分析 test.c:

#include <stdlib.h>
#include<string.h>
#include<stdio.h>
int main()
{
	char * p = malloc(1024);
	p=NULL;
	return 0;
}

上面的代码一眼就看出内存泄漏的问题。之后我们通过执行以下命令进行编译调试:
在这里插入图片描述
gcc test.c -g 其中-g是为了保留符号表,可以定位到代码中具体某一行。
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --undef-value-errors=no --log-file=log ./a.out
tool=memcheck 表示检测内存问题。
leak-check=full 表示完全检测内存泄漏
–log-file=log 表示信息会输入到log文件中(有时文件内容比较多,这样方便分析)

再查看log文件,内容如下:(内容较少)

==9393== Memcheck, a memory error detector
==9393== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9393== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==9393== Command: ./a.out
==9393== Parent PID: 90214
==9393==
==9393==
==9393== HEAP SUMMARY:  //关键信息,表示你的程序内存泄漏的大小
==9393==     in use at exit: 1,024 bytes in 1 blocks
==9393==   total heap usage: 1 allocs, 0 frees, 1,024 bytes allocated
==9393==
==9393== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9393==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==9393==    by 0x40052E: main (test.c:6)
==9393==
==9393== LEAK SUMMARY:
==9393==    definitely lost: 1,024 bytes in 1 blocks
==9393==    indirectly lost: 0 bytes in 0 blocks
==9393==      possibly lost: 0 bytes in 0 blocks
==9393==    still reachable: 0 bytes in 0 blocks
==9393==         suppressed: 0 bytes in 0 blocks
==9393==
==9393== For counts of detected and suppressed errors, rerun with: -v
==9393== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

分析:
由于程序简单,日志内容也比较少。如果log文件内容比较大,分析步骤也是一样的。
第一步:
搜索:HEAP SUMMARY关键字,表示当你程序结束时,内存泄漏的信息统计。很清晰的看到程序有1024个字节没有释放。之后的内容就是泄漏的详细信息(在代码的哪一行,以及泄漏多少字节)
通过log可以看到,在main函数中(test.c的第6行出现了内存泄漏)。大功告成!!!!
如果第一步就找到了泄露位置,那么证明你很幸运。当无法简单一目了然的分析时(log文件内容很多,待会上实际图),你可以参考第二步。
第二步:
搜索:LEAK SUMMARY关键字。表示内存泄漏的类型:

  • definitely lost:确定的内存泄漏,已经不能访问这块内存
    类似下面的代码:
...
char * p = (char*) malloc(1024);
...
p = NULL;
...
  • indirectly lost:指向该内存的指针都位于内存泄露处
typedef struct {
char  *str;
}p_node;
...
struct p_node  *p  = (struct p_node*) malloc(sizeof(struct p_node));
p->str = (char*)malloc(1024);
...
free(p);
...
  • possibly lost:可能的内存泄露,仍然存在某个指针能够访问某快内存,但该指针指向的已经不是该内存首位置
  • still reachable:内存指针还在还有机会使用或者释放,指针指向的动态内存还没有被释放就退出了

definitely lost,indirectly lost 是肯定存在内存泄漏的。而possibly lost ,still reachable 需要我们进一步分析,是否合理。

当进程不会自动停止还能够测试吗?

答案是肯定的。刚开始我也怀疑,于是自己试了一试。只要你运行之后,通过ctrl+c停止,就可以了。同样会生成详细信息(勇于尝试)

实战

上面的列子比较简单,所以很容易分析。现在通过分析项目中的log文件,来加强巩固,先上图:
在这里插入图片描述
如图所示,实际工作中生成的log信息是很多的(8万多行)。
第一步:找HEAP SUMMARY关键字,如图:
在这里插入图片描述
如图所示:大概知道当程序结束时大约还有165M的内存没有释放。并且泄漏的记录有58763条之多。
想要从中找到准确的内存泄漏的地方,还是很难的。

于是我建议执行第二步如图:
在这里插入图片描述
之后再执行valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --undef-value-errors=no --log-file=log-2 taskname调试,比如第一次调试,你让它运行10分钟,第二次让它运行30分钟。同样的环境,如果存在内存泄漏,那么肯定大小不一样。
第二次测试结果:
在这里插入图片描述
之后发现差距主要是在indireactly lost中,之后就在两个文件中搜索are indirectly lost in loss关键字。如图:
log-2文件:
log文件:
在这里插入图片描述
通过定位就很快的找到是video_task.cpp中349行的av_read_frame接口导致的内存泄漏。代码下:

349           if(av_read_frame(pFormatCtx, packet) >= 0)
350           {
351                 timeout = 0;
352                 if (packet->stream_index == videoindex) {
353                         int got_frame = 0;
354
355                         avcodec_decode_video2(pCodecCtx, pFrame,&got_frame, packet);
356                         if (got_frame)
357                         {
358                                 if (pFrame->key_frame)
359                                 {
360                                         int width = pFrame->width;
361                                         int height = pFrame->height;
362                                         tmp_img = cv::Mat::zeros( height*3/2, width, CV_8UC1 );
363                                         memcpy( tmp_img.data, pFrame->data[0], width*height );
364                                         memcpy( tmp_img.data + width*height, pFrame->data[1], width*height/4 );
365                                         memcpy( tmp_img.data + width*height*5/4, pFrame->data[2], width*height/4 );
366                                         cv::cvtColor( tmp_img, bgr, 101 );
367                                         set_image(bgr);
368                                 }
369                         }
370                 }
371               //  av_packet_unref(packet);
372            }

由于对ffmpeg库的不熟悉,网上稍微一搜索,就找到了av_read_frame引起内存泄漏的相关博客,加上371行的av_packet_unref(packet);即可。

至此,内存泄漏的问题就解决了。希望能够帮助到你。

==================================== 杂谈 =====================================

问题

在这个分析和解决的过程中,刚开始我也有自己的一些疑问,不知道你是否和我一样,有不同见解的可以留言一起讨论一下:

  1. 通过log文件分析,为什么会存在那么多没有释放的内存?
    归根而言,还是编码导致的。
    如果编码足够规范,我觉得应该是可以避免的,但是想做到真的很难。
    比如,你的项目中你需要引用一些模块。难道你会对每一个API的实现原理都去研究吗?实际上,我们只要能够实现功能就可以了。比如在ffmpeg库中avformat_find_stream_info接口其实也会产生内存泄漏(我看过很多的blog,基很少有人会去释放)但是为什么没有显现出来呢?那是因为在项目中它只会被执行一次。不会随着时间的推移,造成更大的损失。(当然原则上是不允许的)。所以也就容易被我们忽略。
    在一些大厂中,一般会进行编译静态检测,这是一个非常好的方式,可以将bug尽早发现。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值