比较隐蔽的内存泄露案例分析
和大家分享一个笔者在真实项目中遇到的一个内存泄露真实案例.
- 1. 问题背景
真实项目中的一个待测模块,这里简化一下,整体可以看做是:输入—中间处理–输出模式,如下图1-1
图1-1 待测模块整体框图
其中中间处理与模块业务相关,这里我们可以暂时看做黑盒,不必关心。
- 2. 问题现象
A. 用valgrind跑程序报警图如下1-2,但是报警所指的地方,研发者坚持认为已经释放,是valgrind误报。
图1-2 valgrind报警图
B. 长期压力测试,未见内存使用持续上升,cpu使用率未见异常;
C.不定时会无故退出:排除人工误操作,日志无异常,无core文件,socket通信已屏蔽SIGPIPE信号。
【NOTICE】在Linux下写socket的程序的时候,如果尝试发送到一个断掉的连接上,就会让底层抛出一个SIGPIPE信号。这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。因此我们需要安全的屏蔽SIGPIPE。
- 3. 追查过程
1 )进一步实验的问题现象
这个问题并未引起研发者的注意,研发认为是测试者误操作导致。在程序第一次退出时候,测试者甚至也自我怀疑,是否是误杀了进程或者启动方式有问题。但是测试者在多台机器中启动了程序,观察到了进一步的现象:
A. 都存在文件无故退出问题
B. 退出时间不定,但是都在读完文件之后
C. 观察所有机器的CPU和内存曲线,发现在退出之前都有一个奇怪的波动,波动图案如下图1-3所示:
图1-3 程序退出之前CPU和内存曲线
2 )由现象想到的…
分析图1-3的日志文件,找到图中A,B时间点日志文件,分析文件发现:
A时间点:读完文件最后一条记录的时间点;
B时间点:程序退出的时间点。
由此推断:
(1) 文件读完后,未进入中间业务处理逻辑,CPU未参与计算,所以CPU idle一下子升高了
(2) 但是文件读完以后,内存持续上升,用完资源,最后在B点退出,CPU idle和内存使用恢复正常。
3) 从现象看本质
从以上现象和推论,我们可以大胆的猜想: 程序存在内存泄露!并且在 读文件时候没有泄露,因为长期观察,未见内存持续上涨, 内存泄露发生在文件读完以后。
- 4. 验证猜想
根据猜想翻代码:找到如下代码段,如图1-4和1-5,代码段中我省略了一些与这个bug无关的代码:
图1-4 createTask代码
图1-5 主程序死循环调用createTask
【从现象找代码】从代码标注的步骤,一步一步往下读,可以发现:
A. 研发者只释放了读文件成功时候(input!=NULL)申请的input资源(如图1-4标注的第6步);
B. 文件读失败,返回NULL(如图1-4标注的第三步);外围函数未释放资源,CreateTask函数里面也未释放input资源;
【从代码看现象】从上图1-3和1-4的代码可以解释,问题现象及图1-3的曲线含义:
A. 当输入大数据文件,性能测试的时候,内存未见持续上升(因为读文件成功,正常释放内存);
B. 读完文件,readOnce继续读文件执行失败,返回NULL,外面函数检查返回是NULL,不释放input,而creatTask()死循环不断执行,不断申请内存,不断读文件失败,直到用完内存,程序退出。(所以才会有图1-3的CPU和内存曲线)。
- 5. 如何发现类似问题
A. 认真对待每一次valgrind报警。(事实证明,valgrind所指之处,研发者释放了大部分内存,但在特定场景—即本文中分析的文件读完的场景下,异常处理部分,未释放内存。)
B. 认真观察程序 执行期间以及 执行完毕后,CPU曲线是否异常,内存是否有持续上涨趋势。
C. 代码评审过程中,需要特别注意正常和异常分支是否有资源泄露的可能。
- 6. 总结: 如何避免类似问题
A. 从研发者角度,养成良好的编程习惯,可以将内存分配和释放的过程封装到一个类中,即在构造的时候申请内存,析构的时候释放内存,从而保证没有内存泄露;
B.从测试者角度,代码评审的时候,特别注意:以下函数在资源获取和资源释放时候要对称出现,即在作用域开头申请资源,在作用域末尾释放资源。
malloc/new /new[] 《- -》 free/delete/delete[]
- open/socket/accept/pipe 《- -》 close
fopen/popen 《- -》 fclose
fetch_XXX/get_XXX ßà free_XXX/put_XXX/Release_XXX
*_lock 《- -》 *_unlock