Linux Kernel 模块内存泄露查找 (2)

在之前的一篇博文<<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
++++++++++++++++++++++++++++++


但是某程序员,之前不是自信满满的说“在我机器上跑的好好的"嘛,那么就狠狠的打自己的脸吧!!!在打脸之前还是要恬不知耻的介绍下咱们的Kernel 模块:这个模块名叫KHM(Kernel Hook Module),开源,对Linux中的文件操作进行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。



虽然产品的KHM是开源的,但是为了避嫌,在博文中,并没有很详细的说明KHM的工作流程,也许说明可以将第三部分描述的更加清晰。不过有问题或者错误,还是欢迎博友提出和讨论。^_^









Linux Kernel 模块内存泄露查找 (2),古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。