如何自动检查内存泄漏和句柄耗尽
1. 背景
当程序的子模块数量和规模扩大之后,在开发阶段,系统长时间允许后经常会碰到下面一些bug:
- 内存泄漏。随着时间允许,系统可用的内存越来越少,最后kernel 出现oom 错误;
- 文件句柄耗尽。程序可以打开的文件、套接字、管道越来越少,最后出错在用完了最后一个可用句柄的代码附近;
- 死锁。线程拥有一把锁A,正在申请锁B;但在此时锁B被另外一个线程拥有,且那个线程又在申请锁A。形成一个循环等待、占用且不可释放的状态。
调试这些问题,当然可以从代码流程和逻辑出发,结合ps/gdb/proc/core等命令和信息,一步步挖出root cause。但一般要求对代码、线程关系和相关命令比较熟悉,一般耗时较长。所以,一般大型公司都封装了标准的glibc,做了一个wraaper,然后再wrapper里面加入了对上面调试的支持。还有的可采用专业内存泄漏等检查工具,去做代码检测。那么,对于咱工程师而言,能否能自己设计并实现一个资源检查工具呢?
2. 原理
针对上面的三个例子使用中的资源,我们可以归纳成两类:数量有限的共享资源,比如上面空间有限的内存和数量有限的文件句柄;需要独占的互斥资源,比如上面例子中提到的锁。 下面就分别针对这两种情况,分别展开分析。
2.1 对共享资管的检查
共享资源的特点是:总量有限,通过申请接口获得,使用完了之后通过释放接口归还。为了保证不浪费资源,这就要求程序在使用完了申请得到的资源之后,必须及时释放。而共享资源出现问题的情况,大部分是由于程序员遗忘没有释放造成的。因此,需要一种内部机制能够记录哪些资源使用了还没有释放,可以通过下面的步骤实现:
- 预备一张表,初始化为空;
- 分配的时候,把刚分配的资源的地址等信息记录到表中去;
- 释放的时候,把将释放的资源的地址对应的信息从表中移除;
- 检查的时候:表为空就表明申请的资源都已释放,否则还有资源没被释放;
2.2 对独享资管的检查
独享资源的特点是:互斥使用,基本上是先到先得,通过标志设置是否以备占有。为了保证不死锁,这就要求:程序在申请某个互斥资源的时候,需要检查它已经拥有的资源,是否被它正在申请的互斥资源的拥有者申请。
如果是,会死锁;否则,不会死锁。同样也可以通过下面的步骤实现:
- 预备一张映射表,初始化为空:它描述一个用户拥有哪些互斥资源,同时根据互斥资源能够索引到它的拥有者;
- 分配检查的时候:检查当前用户已经拥有的资源,是否被它正在申请的互斥资源的拥有者申请。同时让这个互斥资源能够索引到当前正在申请它的用户;
- 执行分配的时候:把刚分配的互斥资源的地址添加到当前用户拥有的互斥资源的列表中去,同时让这个互斥资源能够索引到当前拥有它的用户。
- 释放的时候,把将释放的资源的地址从互斥资源列表移除,断开这个互斥资源和当前用户的索引关系。
- 多个用户死锁时的分析:
3. 实现
根据上面原理的分析,我们不难结合之前讲过的xlink、程序堆栈等技巧,选用合适的数据结构来实现。
3.1 共享资源检查的实现
根据2.1中的分析,需要先构造一张表来记录这些资源的地址,这张表要求插入方便,删去也迅速。为此,我们可以用基于平衡二叉树、优先级队列或者hash的方法去实现这个表。对这个表的操作包括PQInsert()/PQRemove()/PQEmpty()等。 此后,就可以开始参考下面列出的针对共享资源泄漏检查的步骤去实现了。
3.1.1 声明支持资源泄漏检查的wrapper函数
可以基于标准的open/close/malloc/free等直接申请、是否公共资源的函数,去实现wrapper。
下面以open()、close()为例,伪码示例如下:
int wrapper_open(char * dev);
int wrapper_free(int fd);
3.1.2 实现支持资源泄漏检查的wrapper函数
还是下面以open()、close()为例:
#define FILE ('f'<<24|'i<<16|'l'<<8|'e')
int wrapper_open(char * dev)
{
int fd = real_open(dev);
PQInsert(FILE, fd);
return fd;
}
int wrapper_close(int fd)
{
int ret = 0;
PQRemove(FILE, fd);
ret = real_close(fd);
return ret;
}
3.1.3 调用支持资源泄漏检查的wrapper函数
有两种方式可以使用支持资源泄漏检查的wrapper函数,一种是代码中之间调用open/close等函数对应的wrapper函数,另外一种是借助gcc Xlink 的支持让标准的open/close函数“重定向”到wrapper_open/wrapper_close函数。显然,后面一种方法工作量最小、最优雅。具体的实现,可以参考前面关于Xlink的博文,下面列出了主要的几个步骤:
-
gcc编译的flag中加入Xlinker改动
Xlinker --wrap=open -Xlinker --undefined=wrapper_open Xlinker --wrap=close -Xlinker --undefined=wrapper_close
-
新的头文件中加入声明
typeof(open) wrapper_open; typeof(close) wrapper_close; typeof(open) real_open; typeof(close) real_close;
3.1.4 检查资源是否泄漏的函数
通常,在程序快要结束退出的时候,会释放资源,末了可以通过共享资源泄漏检查函数去检查是否正的有资源泄漏。这个函数的主要实现的示例如下:
int FileRes_check(void)
{
if PQEmpty(FILE) {
return PASS;
} esle {
PQDump(FILE);
return FAIL;
}
}
3.1.5 记录可能申请了被泄漏的资源代码的位置
根据上一篇博文中,关于如何得到程序堆栈中介绍的方法,我们可以在申请共享资源的时候,具体来说就是调用PQInsert(FILE, fd)时,取得当前程序的stack, 把堆栈信息和fd一起作为一项纪录插入到基于优先级队列、散列或者平衡二叉树实现的表中去。 同样,PQDump()除了打印没有关闭的文件句柄之外,还输出这个句柄对应打开时候的程序堆栈,根据这个堆栈信息,程序员能够定位到打开了这个没被关闭的句柄的代码的位置。而通常,这个句柄的关闭应该在它之后附近。
3.2 独享资源检查的实现
对独享资源死锁的检查的具体实现依赖的技术同上,差别就在死锁检查的逻辑和流程。读者可以自行尝试,等有时间了我也可以再来完善。
4. 总结
通过上面的这么多介绍可以看到,基于对共享资源和互斥资源使用特点的分析,我们能够提出一种针对共享资源泄漏和独享资源死锁检查的通用原理。借助于合适的数据结构(二叉树、优先级队列或散列),基于 Xlinker方法、堆栈获取的API,我们能够实现一种轻巧的、几乎不用改动已有代码的对开发者非常友好的资源检查功能。
本文转自存储之厨51CTO博客,原文链接:http://blog.51cto.com/xiamachao/2065841 ,如需转载请自行联系原作者