一个线程内存泄漏问题定位过程
Posted Arnold Lu@南京
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个线程内存泄漏问题定位过程相关的知识,希望对你有一定的参考价值。
关键词:meminfo、slabinfo、top、pthread_join、thread stack等等。
记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获。
1. 初步定位
是否存在内存泄漏:想到内存泄漏,首先查看/proc/meminfo,通过/proc/meminfo可以看出总体内存在下降。确定内存泄漏确实存在。top中可以显示多种形式内存,进而可以判断是那种泄漏。比如vss/rss/pss等。
确定哪个进程内存泄漏:通过top即可查看到是哪个进程在泄漏。至此基本可以确定到哪个进程。
确定进程泄漏内存类型:然后查看进程的/proc/<pid>/maps,通过maps可以看出泄漏的内存类型(堆、栈、匿名内存等等),有时候运气好可以直接判断泄漏点。
如果是slab:可以通过/proc/slabinfo,可以看出进程的动态变化情况。如果确定是哪一个slab,那么可以在/sys/kernel/slab/<slab name>/alloc_calls和free_calls中直接找到调用点。当然看到的是内核空间的函数。
使用mcheck():可以检查malloc/free造成的泄漏问题,详细参考《2. mtrace/muntrace/MALLOC_TRACE(重复释放、泄漏)》
通过如下脚本,然后对每次抓取内容进行Beyond Compare。每个一定周期抓取相关内存消耗信息。
#!/bin/bash echo > mem_log.txt while true do cat /proc/meminfo >>mem_log.txt cat /proc/<pid>/maps >>mem_log.txt cat /proc/slabinfo >>mem_log.txt sleep 240 done
当然还有其他工具gcc Sanitier、Valgrind等等,由于嵌入式环境受限未能使用。
2. 深入定位
同步查看meminfo、maps、slabinfo,发觉进程虚拟内存损耗很快,远比系统MemFree损耗快。而且slabinfo没有和maps同步损耗。
所以问题重点检查maps问题。
00010000-00083000 r-xp 00000000 b3:11 22 /heop/package/AiApp/AiApp 00092000-00099000 rwxp 00072000 b3:11 22 /heop/package/AiApp/AiApp 00099000-00b25000 rwxp 00000000 00:00 0 [heap] 00b51000-00b52000 ---p 00000000 00:00 0 00b52000-01351000 rwxp 00000000 00:00 0 [stack:30451] 01351000-01352000 ---p 00000000 00:00 0 01352000-01b51000 rwxp 00000000 00:00 0 01b51000-01b52000 ---p 00000000 00:00 0 01b52000-02351000 rwxp 00000000 00:00 0 [stack:30432] 02351000-02352000 ---p 00000000 00:00 0 02352000-02b51000 rwxp 00000000 00:00 0 02b51000-02b52000 ---p 00000000 00:00 0 ... 64f55000-65754000 rwxp 00000000 00:00 0 [stack:28646] 65754000-65755000 ---p 00000000 00:00 0 65755000-65f54000 rwxp 00000000 00:00 0 [stack:28645] 65f54000-65f55000 ---p 00000000 00:00 0 65f55000-66754000 rwxp 00000000 00:00 0 [stack:28642] 66754000-6675a000 r-xp 00000000 00:02 5000324 /usr/lib/AiApp/gstreamer-1.0/libgsticcsink.so 6675a000-66769000 ---p 00000000 00:00 0 ... 6699f000-669a0000 rwxp 00000000 00:02 4999516 /usr/lib/AiApp/gstreamer-1.0/libgstapp.so 669a0000-66a2e000 rwxp 00000000 00:02 4999517 /usr/lib/AiApp/gstreamer-1.0/libgstlive555src.so 66a2e000-66a3e000 ---p 00000000 00:00 0 66a3e000-66a44000 rwxp 0008e000 00:02 4999517 /usr/lib/AiApp/gstreamer-1.0/libgstlive555src.so 66a44000-66a45000 rwxp 00000000 00:00 0 66a45000-66a46000 ---p 00000000 00:00 0 66a46000-67245000 rwxp 00000000 00:00 0 [stack:28631] 67245000-67246000 ---p 00000000 00:00 0 67246000-67a45000 rwxp 00000000 00:00 0 [stack:28630] ... 6b245000-6b246000 ---p 00000000 00:00 0 6b246000-6ba45000 rwxp 00000000 00:00 0 [stack:28613] 6ba45000-6ba46000 ---p 00000000 00:00 0 6ba46000-6c245000 rwxp 00000000 00:00 0 [stack:28610] 6c245000-71066000 rwxs 00000000 00:01 196614 /SYSV5553fc99 (deleted) 71066000-71067000 ---p 00000000 00:00 0 71067000-71866000 rwxp 00000000 00:00 0 [stack:28609] 71866000-71867000 ---p 00000000 00:00 0 71867000-72066000 rwxp 00000000 00:00 0 [stack:28608] 72066000-72228000 rwxs e3dc4000 00:02 6918 /dev/mmz_userdev 72228000-725ac000 rwxs e3a40000 00:02 6918 /dev/mmz_userdev 725ac000-75cac000 rwxs 00000000 00:01 131076 /SYSV6702121c (deleted) 75cac000-75e8a000 rwxs 00000000 00:01 98307 /SYSV6602121c (deleted) 75e8a000-7608e000 rwxp 00000000 00:00 0... 76eeb000-76efb000 ---p 00000000 00:00 0 76efb000-76eff000 r-xp 000ce000 00:02 1234 /lib/libstdc++.so.6.0.20 76eff000-76f01000 rwxp 000d2000 00:02 1234 /lib/libstdc++.so.6.0.20 76f01000-76f08000 rwxp 00000000 00:00 0 76f08000-76f0f000 r-xp 00000000 00:02 1235 /lib/ld-uClibc-0.9.33.2.so 76f1a000-76f1e000 rwxp 00000000 00:00 0 76f1e000-76f1f000 rwxp 00006000 00:02 1235 /lib/ld-uClibc-0.9.33.2.so 76f1f000-76f20000 ---p 00000000 00:00 0... 7c720000-7cf1f000 rwxp 00000000 00:00 0 [stack:30574] 7cf1f000-7cf20000 ---p 00000000 00:00 0 7cf20000-7e121000 rwxp 00000000 00:00 0 [stack:30575] 7eef7000-7ef18000 rwxp 00000000 00:00 0 [stack] 7efb7000-7efb8000 r-xp 00000000 00:00 0 [sigpage] ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
通过多次maps对比,可以发现[stack:TID]类型的内存以及一个匿名内存在不停增加消耗内存。
其中[stack:TID]类型的内存,在内核查找相关代码没有明确对应属性。初步判断是线程的栈,TID表示线程id号。
所以这里应该是某个线程泄漏。
2.1 线程栈泄漏(Joinable线程栈)
一个导致线程栈泄漏原因可能是对于一个Joinable线程,系统会创建线程私有的栈、threand ID、线程结束状态等信息。
如果此线程没有pthread_join(),那么系统不会对以上信息进行回收。这就可能造成线程栈等泄漏。
确定线程栈泄漏的方法是:通过ls /proc/<pid>/task | wc -l确定进程下线程数目。然后在maps中检查[stack:TID]数目。两者如果不一致,则存在Joinable线程没有调用pthread_join()造成的泄漏。
如果maps没有[stack:TID],可以通过pmap <pid> | grep <stack size> | wc -l,即通过检查栈大小的vma数目来确定栈数目。
关于线程内存泄漏参考:《Avoiding memory leaks in POSIX thread programming》
3. 问题根源
通过检查线程栈消耗与实际线程数目,发现两者数目吻合。所以线程并没有退出。也即不是由于未使用pthread_join()导致的内存泄漏。
然后根据maps中[stack:TID]的pid号,cat /proc/<pid>/comm发现是同一个线程不停创建。但是没有释放。
其实通过top -H -p <pid>和maps也可发现问题,中间走了弯路。
所以问题的根源是,进程不停创建但是没有退出造成内存消耗殆尽。
4. 收获
有两个收获,一是创建的pthread线程Join和Detach两种状态下内存处理差别;二是在进程maps中显示线程栈[stack:TID]更有利于调试。
4.1 pthread线程的join和detach区别
《Avoiding memory leaks in POSIX thread programming》讲到如何避免POSIX线程编程时内存泄漏。
首先pthread_create()创建的线程默认是joinable的。
对于joinable线程,系统会分配私有内存存储线程结束状态、线程栈、线程ID等等资源。这些资源会一直存在,直到线程结束并且线程被其他线程joined。所以确保joinable线程资源得到释放的两个条件是:线程退出、被其他线程joined。
对于detached线程,如果其退出,那么系统会自动回收其占用的资源。
关于joinable线程没有被其他线程joined造成内存泄漏的实验。
#include<stdio.h> #include<pthread.h> void run() { pthread_exit(0); } int main () { pthread_t thread; int rc; long count = 0; while(1) { if(rc = pthread_create(&thread, 0, run, 0) ) { printf("ERROR, rc is %d, so far %ld threads created\\n", rc, count); perror("Fail:"); return -1; } usleep(10); count++; } return 0; }
输出结果如下:
ERROR, rc is 11, so far 32751 threads created Fail:: Cannot allocate memory
总共创建了32571个线程,造成内存消耗殆尽。
通过对比中间过程的maps,可以发现每次增加一个8MB的栈以及一个分隔页。
在pthread_create()之后增加pthread_join()则内存非常稳定。
#include<stdio.h> #include<pthread.h> void run() { pthread_exit(0); } int main () { pthread_t thread; int rc; long count = 0; while(1) { if(rc = pthread_create(&thread, 0, run, 0) ) { printf("ERROR, rc is %d, so far %ld threads created\\n", rc, count); perror("Fail:"); return -1; } pthread_join(thread, NULL); usleep(10); count++; } return 0; }
借用文档里面一句话总结一下:Joinable threads should be joined during programming. If you are creating joinable threads in your program, don’t forget to call pthread_join(pthread_t, void**)
to recycle the private storage allocated to the thread.
调用pthread_join()将阻塞线程自己,一直等到加入的线程运行结束。
线程可以分为两种:joined和detached。并不是所有线程创建后都默认joinable,需要显式指定属性。
joinable线程在创建后,可以通过pthread_detach()显式分离。在分离后,不可以再合并。
如果一个线程结束运行,但没有被join。则它的状态类似进程中的Zombie Process,即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join()来等待线程结束,并可得到线程的退出代码,回收其资源。
如果父进程调用pthread_detach(child_thread_id)或者子进程调用pthread_detack(pthread_self())即可将子进程状态设置为detached,该程序运行结束后会自动释放所有资源。
4.2 关于在maps中显示[stack:TID]
在进程maps中显示线程栈信息,最后在内核中被放弃。
首先在《procfs: mark thread stack correctly in proc/<pid>/maps》中,添加了[stack:TID]用于表示此vma对应的是线程TID的stack区域。
这样做的好处是,可以从maps中明确知道此段vma是被哪个线程使用的。
有一个坏处就是先线程非常多情况下,主线程中为了显示[stack:TIS],开销就会很大,而实际上用处不是很大。
所以在《proc: revert /proc/<pid>/maps [stack:TID] annotation》将进程maps中的[stack:TID]删除了,只显示为匿名内存。
最终再《fs/proc: Stop trying to report thread stacks》将所有[stack:TID]全部移除。
那么在没有[stack:TID]的情况下如何断定vma是否是线程栈呢?
首先线程栈大小可以通过ulimit -s查看,所以maps中vma大小和这个一致;并且属性应该是匿名的rw-p。
然后上面应该是一页大小作为分隔区间,分隔页的属性应该是---p。
以上是关于一个线程内存泄漏问题定位过程的主要内容,如果未能解决你的问题,请参考以下文章
Java内存泄漏分析系列之一:使用jstack定位线程堆栈信息