最近忙于处理一个项目,项目使用capwap进行同步多台设备的配置和固件。使用了开源的 opencapwap0.93.3 进行开发,但是遇到了遇到了内存泄露的问题。本想内存泄露使用工具应该比较好检查出来。
但是期间一波三折,这里作为经验部分记录下来。同时编写了小工具处理该类问题,分享在 Github 点我直达
环境介绍
由于程序运行于我们的一个特殊设备中(可以理解为路由器),设备可用资源很有限,采用C语言进行代码编写。
部分细节:
- 设备采用SPI FLASH存放只读文件系统,大小仅16MB。
- 设备运行的linux极为精简,编译后固件只有10MB。
- 设备可供调试、运行程序使用的分区为 var 目录,可用大小约为100MB。
- ssh为精简版,丢掉了sftp等。
- 常用软件全来自busybox。
一、绝望的尝试
总共尝试了好几种工具进行内存泄露的检测,结局不是失败就是半失败状态。
Valgrind:虽然说这工具是神器,但是无可奈何我们系统不支持 LD_PRELOAD,结果也想得到,没法检测。而且因为链接了openssl,导致了很多报错,辣眼睛没法看。
Dmalloc:尝试了好几种工具都失败了后看到程序编写者曾经使用Dmalloc进行内存泄露的检查。通过开启该方式,成功找到了几个内存问题点。但是解决掉这几个点之后,干脆Dmalloc连log文件都不生成了。但是内存依然还有问题,必须想办法其他解决。
二、柳暗花明的曙光
经过好长一段时间的挣扎看源码,忽然发现了一个规律。代码中将malloc 和free 进行封装在一个宏定义中,这让我看到了曙光。我总结了以下一种简单的查找内存泄露的办法,尤其在代码量大,不好直接看代码或逻辑异常绕且调试受限的场合。
2.1 处理 malloc 函数
利用宏定义将malloc函数封装一遍,之后调用malloc的地方全部换成该宏进行代替。如下代码所示,如果启用 JET_DEBUG 宏,那么将会在console 中打印出相关内存申请与释放消息。可以根据自己系统实际情况将其打印到文件或者其他console口中。
#ifdef JET_DEBUG
/*
* jet malloc description
* only print information to console
* example :
* assert_memory(__FILE__,__func__,__LINE__,obj_name,1); //malloc
* assert_memory(__FILE__,__func__,__LINE__,obj_name,0); //free
*/
void static const assert_memory( const char * file_name, const char * func_name, unsigned int line_no ,void *obj_name , char * mode)
{
if( mode == 1){
printf("jet_assert malloc:%s, func: %s,line %u\n", file_name, func_name, line_no );
printf("jet_malloc:0x%08x\n",(unsigned int)obj_name);
}else{
printf("jet_assert free :%s, func: %s,line %u\n", file_name, func_name, line_no );
printf("jet_free:0x%08x\n",(unsigned int)obj_name);
}
}
#define CW_FREE_OBJECT(obj_name) {if(obj_name){assert_memory(__FILE__,__func__,__LINE__,(void *)obj_name,0);free((obj_name)); (obj_name) = NULL;}}
#define CW_CREATE_OBJECT_ERR(obj_name, obj_type, on_err) {obj_name = (obj_type*) (malloc(sizeof(obj_type))); assert_memory(__FILE__,__func__,__LINE__,(void *)obj_name,1);if(!(obj_name)) {on_err}}
#else
#define CW_FREE_OBJECT(obj_name) {if(obj_name){free((obj_name)); (obj_name) = NULL;}}
#define CW_CREATE_OBJECT_ERR(obj_name, obj_type, on_err) {obj_name = (obj_type*) (malloc(sizeof(obj_type))) ;if(!(obj_name)) {on_err}}
#endif
目的就是希望发生内存泄露的时候能够根据console口中打印出来的信息进行判断哪里申请了内存,哪里释放了内存。
2.2 日志格式
因为程序比较大,相印申请和释放内存将极为频繁。所以建议将以上log信息存放在一个文件中,这里我们将其命名为 memory.log。
同时我们的程序应当是循环的(这里说循环是指类似于:接受指令->完成任务->接受指令->完成任务)。所以我们找到接受指令/完成任务 的代码,修改添加打印出标记信息。最终结果应像下面范例文本:
memory.log
======>>start
jet_assert malloc:ACInterface.c, func: CWInterface,line 717
jet_malloc:0x015af878
jet_assert free :CWThread.c, func: CWHandleTimer,line 720
jet_free:0x015b1f60
jet_assert free :CWThread.c, func: CWHandleTimer,line 721
jet_free:0x015ae480
jet_assert malloc:CWThread.c, func: CWTimerRequest,line 733
jet_malloc:0x015b1c08
jet_assert malloc:CWThread.c, func: CWTimerRequest,line 734
jet_malloc:0x015d32e8
======>>end
三、小工具助力起飞
得到以上日志文件后,可能一段log就有几千上万行。如果毅力好的人可以手动找到malloc申请到的地址和free释放的地址匹配,并删除相关条目,一般来说存在内存泄露的泄露最后都会留下那么几行 malloc 信息。按照指示去一步步修改应该就没有问题了。
但是,我是一个懒人,一行一行的手动删除去除一对malloc和free的,我可能要疯掉。
所以我写了一下一个python小工具,按照上面所列举出的日志格式进行处理。极大的提高了我的效率。
注意
- 该python代码适合处理windows文本类型的字符串,如果直接从console中复制出来的文件,请使用 notepad++ 这类工具转换一下,在 编辑->文档格式转换->转为Windows格式。
- log文件名字为 memory.log,且应该与python脚本在同一个目录下。
- 脚本会剔除匹配的 malloc 与 free 项目,留下没有匹配的项目。
- 建议准备连续两段的log日志进行分析,因为程序时常这一轮申请的资源在下一轮中进行释放,这样子更易于分析。
- python新手,代码辣眼睛,用不了的根据自己实际情况修改。