在之前的一篇博文<<Linux Kernel模块内存泄露的一种查找思路>>中,我介绍了一种查找内核内存泄露的一种方法。这不才几个月,又有客户埋怨:使用了产品5天左右后,Suse服务器由于内存耗尽而Crash。O My God,不会吧,在我机器上跑的好好的哇(程序员常用名言 嘿嘿)。 那么就让我们一起来看看,苦逼的博主是如何确定问题并且找到问题的....
一. 确定问题
第一步,我们要做的是,确定这个问题和产品的Kernel模块有关系。首先根据客户描述,如果停止我们产品,则不会出现内存泄露问题。那确定问题和我们产品有关系,但是和用户态程序还是内核模块程序有关系呢?根据客户提供的Kernel Dump查看Slab占用3.6G。那么十有八九,是产品Kernel模块存在Memory Leak了。
crash> kmem -i PAGES TOTAL PERCENTAGE TOTAL MEM 981585 3.7 GB ---- FREE 24987 97.6 MB 2% of TOTAL MEM USED 956598 3.6 GB 97% of TOTAL MEM SHARED 46 184 KB 0% of TOTAL MEM BUFFERS 36 144 KB 0% of TOTAL MEM CACHED 10 40 KB 0% of TOTAL MEM SLAB 941424 3.6 GB 95% of TOTAL MEM TOTAL SWAP 1048575 4 GB ---- SWAP USED 527 2.1 MB 0% of TOTAL SWAP SWAP FREE 1048048 4 GB 99% of TOTAL SWAP
但是某程序员,之前不是自信满满的说“在我机器上跑的好好的"嘛,那么就狠狠的打自己的脸吧!!!产品实现了一个内核级别的I/O Hook去进行特定的操作。
博主写了个脚本,不断的拷贝文件,模拟出大量的I/O操作,这样就会不断触发调用产品内核模块的Hook函数。在测试之前记录内存使用情况,先使用如下命令清除系统使用缓存:
SUSE11X64-001:~ # sync SUSE11X64-001:~ # echo 3 > /proc/sys/vm/drop_caches
然后记录内存使用情况,主要记录空闲内存和Slab使用内存:
SUSE11X64-001:~ # cat /proc/meminfo MemTotal: 1989340 kB MemFree: 1495368 kB ...... Slab: 37752 kB
......
然后等待3天(刚好过个周末~~~),使用如上同样方法查看当前空闲内存和Slab使用内存情况,最后发现3天内消耗大约300M内存,刚好约为Slab增长的内存。这样算下Memory leak Rate大概为4.2 M/hour. 也就是说,如果不是通过脚本模拟出大量的I/O操作,将会有更小的Memory Leak Rate,确实不易发现内存泄露。既然问题确定了,那么结下来就进行Memory Leak分析啦。
二. 问题分析
在对这个问题进行分析之前,我们分析下客户提供的Kernel Dump,Slab中哪种类型的Cache占用了太多的内存:sock_inode_cache占用了大约1.8G内存, dentry大约占用了700多M内存。
crash> kmem -s
CACHE NAME OBJSIZE ALLOCATED TOTAL SLABS SSIZE
......
ffff880138431300 sock_inode_cache 640 2842421 2842524 473754 4k
......
ffff880138c00e00 dentry 192 3769490 3769880 188494 4k
sock_inode_cache在内核中存储socket的内核结构,而dentry则对应文件或者目录在内核中的数据结构,如果你和我一样,对Linux的内核还没有特别精通的情况下,那么首要的怀疑目标就是dentry。在内核模块中会对文件的dentry进行访问,那么如何引起内存泄露的呢?这时有以下两个怀疑的思路:
(1) 采用kmalloc等api申请内存空间然后没有释放;
(2) 在对dentry引用访问后,没有对其引用计数进行释放,比如调用dget之后,并没有相应的调用dput.
然后通过Code Review排除了情况(1),但是针对情况(2)也进行了查看,发现在访问dentry后,都调用了dput减少一次引用计数。这个问题一直深深的困扰着我,一个星期以来都不愿意再看这个问题了,可是问题总归要解决的啊??也想了一些方法,比如使用kmemleak?但是得重新编译所有的Suse内核源码,并且不一定能够很清楚的查询到Memory Leak的原因,鉴于我们产品内核模块的代码量不是很大,最终决定,再一次进行Code Review。2天半的时间,功夫不负有心人,终于找到了根本原因!
三. 根本原因
程序执行流程如下:
(1) 根据文件fd,获取file对象,从file对象中获取path对象,并使用path指针pPath记录path对象地址(path对象中包扩了dentry和vfsmount成员指针)。
(2)程序中需对dentry和vfsmount进行访问,于是采用path_get(pPath)对引用计数加一
(3)调用系统中的原始的close,来关闭文件
(4)进行一系列的操作后,采用path_put(pPath)对dentry和vfsmount引用计数减一
问题就出在第(3)步和第(4)步上:如果只有一个进程对文件打开并进行了访问,然后关闭文件,则进入我们产品的Hook函数,当进入第三步的时候,调用系统原始的close,内核中将进行如下调用过程:close->filp_close->fput->__fput. 可以看到在fput中,如果当前file对象的引用计数只为1的时候,才调用__fput.
void fput(struct file *file)
{
if (atomic_long_dec_and_test(&file->f_count))
__fput(file);
}
一般情况下file对象此时引用计数为1(例外比如一个进程打开文件并且fork),调用__fput,注意其中会将其dentry和mnt指针设置为NULL。
static void __fput(struct file *file)
{
struct dentry *dentry = file->f_path.dentry;
struct vfsmount *mnt = file->f_path.mnt;
struct inode *inode = dentry->d_inode;
might_sleep();
fsnotify_close(file);
/*
* The function eventpoll_release() should be the first called
* in the file cleanup chain.
*/
eventpoll_release(file);
locks_remove_flock(file);
if (unlikely(file->f_flags & FASYNC)) {
if (file->f_op && file->f_op->fasync)
file->f_op->fasync(-1, file, 0);
}
if (file->f_op && file->f_op->release)
file->f_op->release(inode, file);
security_file_free(file);
ima_file_free(file);
if (unlikely(S_ISCHR(inode->i_mode) && inode->i_cdev != NULL &&
!(file->f_mode & FMODE_PATH))) {
cdev_put(inode->i_cdev);
}
fops_put(file->f_op);
put_pid(file->f_owner.pid);
file_sb_list_del(file);
if ((file->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
i_readcount_dec(inode);
if (file->f_mode & FMODE_WRITE)
drop_file_write_access(file);
file->f_path.dentry = NULL;
file->f_path.mnt = NULL;
file_free(file);
dput(dentry);
mntput(mnt);
}
因为在第(2)步我们对dentry和mnt进行了引用计数加1(此时引用计数为2),那么在__fput中调用dput和mntput只会对其引用计数减一,但使用内存并不会进行释放。而理论上应该在第(4)步进行完我们定义的操作之后,对其进行释放。可是!!!我们在第(4)步的时候调用了path_put(pPath)去进行释放,可这时候因为之前调用了原始的close,path中的dentry和mnt指针早已被设置为NULL,所以在第(4)步的时候,dentry和mnt并没有进行引用计数减一,进而也没有释放内存,从而造成了内核的Memory Leak。