我的内核模块在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结构没有释放导致的。
%{ #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、盲目地相信自己的代码。每个人都会犯这样的错误,当代码出现问题的时候,只把注意力放在功能复杂的地方或者自认为会出问题的地方,而忽略不太可能出问题的地方。有时问题恰恰就出在不易出现问题的地方。