内核中accept连接时创建socket结构错误导致的内存泄露

来源:互联网 发布:淘宝联盟引流人数 编辑:程序博客网 时间:2024/06/06 19:20
  强烈推荐一下淘宝褚霸的这篇文章: http://blog.yufeng.info/archives/2456
  注:这里我们只关心TCP套接字,所以文章中说sock结构或者socket结构的时候都只针对TCP协议。
 
  在测试内核模块时,内核会因为内存耗尽而panic,使用crash工具查看core文件,提示的信息是"Kernel panic - not syncing: Out of memory and no killable processes..."。也就是说内核没有内存可以使用,并且也没有进程可以杀死来释放内存。不用说,肯定是发生了内存泄露!写这篇文章是为了总结一下查找这个BUG的过程,反思一下。在这个过程中用到了很多工具和方法,写下来对遇到类似问题的人或许有些帮助。
  内核中运行的程序主要是我的内核模块和应用层的HTTP服务器。所以造成内核泄露要么是内核模块,要么是HTTP服务器。
  问题出在HTTP服务器的可能性不大,首先这个服务器已经上线了挺长时间,相对比较稳定;其次如果是应用进程占用的内存过多,内核完全会杀死这个用户进程来释放内存,也就不会出现“Out of memory and no killable processes”这样的信息。所以基本可以肯定问题出在内核模块上。为了保险,还是要看一下应用层的服务器进程占用了多少内存。其中一台服务器的内存比较小,所以在这个机器上问题很快就可以重现。在内存消耗量比较大(通过free -m可以看到)时,通过top命令查看应用服务器进程占用的RSS内存(也可以通过/proc/[pid]/statem文件查看),应用服务器占用的内存只有100多M。接着查看进程占用的文件数,也就是连接数,这个信息可以通过/proc/[pid]/fd目录获取。应用服务器占用的连接数也非常少。通过查看应用服务器实际占用的内存数和连接数,现在就要开始去内核模块中查找问题了。

  我的内核模块在sysfs中注册了相应的项,通过这些可以看到当前占用的内存数量。这些项的统计信息都是在分配内存成功、释放之后更新的,可以实时反映当前的内存占用信息。为了方便,我编写了一个脚本,每隔一秒就把这些信息刷到终端上,如下图所示:


  total_mem项的单位是KB,所以这里看到占用的内存大约为3M。内存统计的地方都已经反复地检查过了,所以这个量是可信的。这个结果不是我想看到的,如果统计出来的量大的话,我就会从内核模块分配和释放的地方去找问题。但是现在这个量这么小,很明显不是内核模块中调用kmalloc()或kmem_cache_alloc()(内核模块调用)分配的内存没有被释放。这个时候很容易陷入迷茫,估计很多人都会去反复地检查自己的代码,看看是否有内存没有释放的地方。如果我最开始也这样做的话,或许也能早点发现BUG,但是也有可能要花更长的时间。一般情况下,代码中有很多不同功能的模块,每个模块的功能复杂度不同。你去检查代码往往会忽略功能简单的模块,即使是在遇到BUG的时候。而且这样盲目地去遍历各个可能出问题的地方,也很不明智,因为你现在了解到的信息还太少。所以,这个时候,首先要做的不是去检查代码,而是首先要先通过各种工具和手段,来看一看内存究竟用到了什么地方。你现在遇到的问题,很多前辈们也遇到过。为了方便快捷地解决遇到的问题,前辈们已经为我们开发出了很多很多方便好用的工具,为什么不拿来用呢?貌似扯的有点远了,继续我们的问题.......
  很庆幸看到了霸爷的《Linux Used内存到底哪里去了?》这篇文章,所在我决定先按照这篇文章的方法,找到系统被使用的内存都在哪里。正如文章中所说的,内存主要用3个去向:进程消耗、slab消耗和pagetable消耗。其中pagetable消耗是内核管理页面时的消耗,也就是struct page等结构的消耗。slab消耗不仅包括管理slab结构本身的消耗,还包括每个slab缓存的内存。slab缓存的内存在这三类中占用的量也是很大的。我们知道内核模块或驱动、内核活动本身分配内存,都是基于slab的。kmalloc()分配的内存也是从slab中获取的。按照霸爷的方法,我计算除了进程占用的内存、slab消耗的内存和pagetable占用的内存,这个值和free命令看到的基本吻合。这两个值会有偏差,因为会对共享库占用的内存重复计算。只要差别不大,就可以。我为什么要按照文章中的方法自己做了一次呢?很简单,首先我可以熟悉一些工具的使用,了解这些工具可以提供给我什么信息;其次,在这个过程中我可以知道怎么从/proc目录下找到一些内存相关的项,通过这些项可以看到系统的运行情况。
  OK,现在计算了系统使用的内存量,并且和free看到的一样。但是这个时候我急于解决问题,心浮气躁,犯了一个很大的错误。我已经计算了进程消耗的内存量、slab消耗的内存量、pagetable消耗的内存量,这时我至少应该看一看这三个值哪个比较大吧?看看内存究竟在哪个地方消耗的多吧,可是我竟然没有!深深地鄙视一下自己!如果这时我只要留心看一下,我就会发现slab占用的内存非常大(后话,情况确实如此),这可以大大缩短我消耗的时间。如果发现slab占用的内存太大,可以用slabtop这个工具来看看哪个slab占用的内存过多(不同的slab有不同的名称)。slabtop显示的信息是根据/proc/slabinfo这个文件生成的。很幸运,虽然我犯了一个错误,可我还是使用了slabtop这个工具看查看slab消耗的内存情况(现在也不知道当时为什么要这样做)。当然这个时候我看这个工具的输出完全没有目的性,胡乱的看,第一次用总是很茫然。在看的过程中,我注意到了一个叫”TCP“的Slab的缓存大小一直在增加,而且增加的速度也挺快的。这时我意识到问题可能出现在这个Slab上,果断重启机器重新构造环境,并且单独打开了一个终端,在这个终端运行slabtop,这样在内核panic之后,你在终端(Xshell)仍然可以看到最近时间的slabtop输出。程序启动之后,我就开始在内核源码中找”TCP“对应哪个slab。首先从名称来看,这个肯定和TCP协议相关。看过内核协议栈的肯定都知道,当分配套接字的时候,会从tcp_prot变量的slab成员中分配sock结构,我首先找这个slab的名称是什么。tcp_prot变量的slab成员在定义的时候是没有初始化的,它是在注册到系统的时候才初始化,注册是在inet_init()函数中进行的,如下所示:

static int __init inet_init(void){    ......    rc = proto_register(&tcp_prot, 1);    if (rc)        goto out;    ......}
  而slab的创建是在prot_register()函数中,如下所示:

int proto_register(struct proto *prot, int alloc_slab){    if (alloc_slab) {        prot->slab = kmem_cache_create(prot->name, prot->obj_size, 0,                    SLAB_HWCACHE_ALIGN | prot->slab_flags,                    NULL);        if (prot->slab == NULL) {            printk(KERN_CRIT "%s: Can't create sock SLAB cache!\n",                   prot->name);            goto out;        }    ......    }    ......}
结合上面两部分代码,可以看到这里创建的slab的名称是tcp_prot的name成员,那这个name成员是什么呢?看看tcp_prot的定义就知道了,如下所示:

struct proto tcp_prot = {    .name            = "TCP",    .owner            = THIS_MODULE,    ......};
 没错,就是看到的那个"TCP"slab就是tcp_prot的slab成员对应的那个。tcp_prot的slab是在inet_create()中创建sock结构的时候用到。此时"TCP"slab占用的内存过多,是因为有太多的sock结构没有释放导致的。
  我的内核模块是用来做TCP连接迁移,会在两个地方创建sock结构,一个是在内核中创建新的连接时(和传统的方式不一样),一个是在发送迁移信息的时候(正常的TCP连接)。因为构建的连接在成功之后放到系统中了,后期的释放完全按照TCP协议的正常关闭流程,不由内核模块控制了,如果是这造成的,不好确定。所以先确定是否是因为发送迁移信息时创建的连接没有关闭导致的。好好检查了一番代码,基本排除了这种可能。下面就是我也确定是否是因为我创建的连接没有释放导致的,最先想到的就是在sk_free()处添加钩子(也就是kprobe机制)。当然,有Systemtap这个强大的脚本工具,自然不会再编写C代码,然后用insmod或modprobe插入模块的方式了。下面是我的sk_free.stp脚本:
%{#include <linux/tcp.h>#include <net/tcp.h>#include <linux/sched.h>%}function sk_free_test:long(arg1:long) %{    struct sock *sk = (struct sock *)THIS->arg1;    if (inet_sk(sk)->sport == htons(80)) {        _stp_printf("func:sk_free_test, sk=%p, sk->sk_state=%u, sk->sk_wmem_alloc=%d,slab:%p,daddr=%d.%d.%d.%d\n",                sk, sk->sk_state, atomic_read(&sk->sk_wmem_alloc), sk->sk_prot_creator->slab, NIPQUAD(inet_sk(sk)->saddr));    }    THIS->__retvalue = 0;    return;%}probe begin{    printf("Systemtap scripts start......\n");}probe kernel.function("sk_free"){    sk_free_test(pointer_arg(1));}
 创建的连接的本地端口是80,输出结构如下所示:


  红色圈住的部分是比较关键的,当sk->sk_wmem_alloc的值为1时,在sk_free()中会立即调用__sk_free()来释放sock结构,kmem_cache_free()是在__sk_free()中调用的。
  根据输出信息,内核模块创建的连接已经被释放了。使用ss命令查看,也没有80端口的连接存在。内核创建的所有sock结构都存储在tcp_hashinfo散列表中。这个时候我想的就是,如果sock结构没有被释放,一定可以在tcp_hashinfo中找到(现在不这么想了......)。为了进一步确定,编写了下面的Systemtap脚本,来遍历tcp_hashinfo来查看到底有多少个sock结构,如下所示(因为time_wait状态下的sock结构占用的内存属于"tw_sock_TCP"slab,其实不用遍历twchain的):

%{#include <linux/tcp.h>#include <net/tcp.h>%}  function tcp_info_test:long() %{    int i;    struct inet_ehash_bucket *head;    struct sock *sk;    const struct hlist_nulls_node *node;    unsigned long es = 0;    unsigned long tw = 0;    unsigned long es_sum = 0;    unsigned long tw_sum = 0;    unsigned int port = 8090;    local_bh_disable();    rcu_read_lock();    for (i = 0; i < tcp_hashinfo.ehash_size; ++i) {        head = &tcp_hashinfo.ehash[i];        sk_nulls_for_each_rcu(sk, node, &head->chain) {            /*            if ((sk->sk_state == TCP_ESTABLISHED) &&                    inet_sk(sk)->sport == htons(80)) {                    */            ++es_sum;            _stp_printf("sock:saddr=%d.%d.%d.%d, daddr=%d.%d.%d.%d, sport=%u, dport=%u.\n",                    NIPQUAD(inet_sk(sk)->saddr),                     NIPQUAD(inet_sk(sk)->daddr),                     ntohs(inet_sk(sk)->sport),                    ntohs(inet_sk(sk)->dport));            if ((inet_sk(sk)->sport == htons(port)) ||                    (inet_sk(sk)->dport == htons(port))) {                _stp_printf("in chain, sock state:%u.\n", sk->sk_state);                es++;            }        }    }    for (i = 0; i < tcp_hashinfo.ehash_size; ++i) {        head = &tcp_hashinfo.ehash[i];        sk_nulls_for_each_rcu(sk, node, &head->twchain) {            /*            if ((sk->sk_state == TCP_TIME_WAIT) &&                    inet_twsk(sk)->tw_sport == htons(80)) {                    */            ++tw_sum;            if ((inet_twsk(sk)->tw_sport == htons(port)) ||                    (inet_twsk(sk)->tw_dport == htons(port))) {                _stp_printf("in twchain, sock state:%u.\n", sk->sk_state);                tw++;            }        }    }    rcu_read_unlock();    local_bh_enable();    _stp_printf("Established: %lu; TimeWait: %lu, ES_Sum:%lu, TW_sum=%lu.\n", es, tw, es_sum, tw_sum);    THIS->__retvalue = 0;    return;%}probe begin{    printf("Systemtap scripts start....\n");    tcp_info_test();    exit();}
输入的结果如下所示:


属于80端口的sock结构要么没有要么就3、4个(执行时机不同),所以80端口的sock结构占用的内存很少,而且tcp_hashinfo散列表中总的sock数量也很少,这个数量保持的很稳定,不会一直增长。
  现在真是彻底迷茫了,因为在我的知识里,我就认为sock结构一定在tcp_hashinfo散列表中。那么多分配的sock结构究竟去哪了呢?即便是这个时候,也不要盲目地去反复检查代码,应该冷静一下。内核模块创建的sock结构可以正常提供服务,并且通过脚本可以确定已经正常关闭了,所以这个地方没有问题。发送迁移信息时,使用的TCP连接是正常的TCP连接,完全由内核处理的,也不会有问题。反复想了一下,觉得可能是在内核中使用TCP连接的方式不对,因为我突然想到在主动发起连接的时候是调用sock_create()创建的socket结构,在accept连接的时候也是调用的sock_create()创建socket结构。可是这种方式是参考TCPhandoff(我的内核模块就是借鉴这个开源项目的思想)写的,应该不会有问题吧?最终还是感觉不对劲,去内核代码中看看sock_create()的实现,看完之后终于找到问题出现在哪了!sock_create()在创建socket结构的同时,也会初始化其sk成员,也就是说会创建一个sock结构实例存储在socket结构的sk成员上。当你在内核中用socket结构实例调用accept操作的时候,就是要从中取出一个连接,也就是sock结构实例,这个实例会存储在socket结构的sk成员上。所以造成内存泄露的原因就是,我在accept之前错误地调用了sock_create()来创建socket结构,这样创建的socket结构的sk成员的sock结构实例(这个实例不是代表一个连接,只是一个无意义的sock实例)在accept操作中会被覆盖。所以我应该调用的sock_alloc()这样的接口来创建socket结构,而不是sock_create()。这段代码是项目刚开始的时候写的,那个时候对内核协议栈很不熟悉,所以照葫芦画瓢,就稀里糊涂地把tcphandoff的错误给继承了下来。当然,这个时候我也知道完全没必要去先创建socket结构,然后用socket->ops->accept这样的方式来accept连接,完全直接用kernel_accept()就行了。
  这个BUG的解决也改变了我的一个认识,内核创建的sock结构不一定都在tcp_hashinfo散列表中。为了方便说明,我们从用户态的角度来说。假设你这时要发起主动连接,你会先调用socket()函数创建一个fd,这个fd对应的sock结构在你没有执行connect操作之前,它是不会放在tcp_hashinfo散列表中的。当然你在建立listen套接字的时候也是如此。
  总结一下经验教训,希望别人不要犯同样的错误:
  1、没有好好利用得到的信息。前面提到的,我已经得到了slab占用的内存信息,却没有好好利用。
  2、盲目地怀疑工具。当ss显示的套接字数量很少时,我怀疑是工具的问题,所以自己写了脚本来统计
  3、盲目地迷信别人的代码。在你参考别人的代码时,最好弄清楚它的意图是什么,也要思考一下它这样做对不对,不要盲目地照搬。
  4、盲目地相信自己的代码。每个人都会犯这样的错误,当代码出现问题的时候,只把注意力放在功能复杂的地方或者自认为会出问题的地方,而忽略不太可能出问题的地方。有时问题恰恰就出在不易出现问题的地方。

原创粉丝点击