记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获。
是否存在内存泄漏:想到内存泄漏,首先查看/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等等,由于嵌入式环境受限未能使用。
同步查看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号。
所以这里应该是某个线程泄漏。
一个导致线程栈泄漏原因可能是对于一个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》
通过检查线程栈消耗与实际线程数目,发现两者数目吻合。所以线程并没有退出。也即不是由于未使用pthread_join()导致的内存泄漏。
然后根据maps中[stack:TID]的pid号,cat /proc/<pid>/comm发现是同一个线程不停创建。但是没有释放。
其实通过top -H -p <pid>和maps也可发现问题,中间走了弯路。
所以问题的根源是,进程不停创建但是没有退出造成内存消耗殆尽。
有两个收获,一是创建的pthread线程Join和Detach两种状态下内存处理差别;
二是在进程maps中显示线程栈[stack:TID]更有利于调试。
《Avoiding memory leaks in POSIX thread programming》讲到如何避免POSIX线程编程时内存泄漏。
https://developer.ibm.com/tutorials/l-memory-leaks/
首先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个线程,造成内存消耗殆尽。
在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,该程序运行结束后会自动释放所有资源。
在进程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]删除了,只显示为匿名内存。
Just as in other memory leaks, the problem may not be obvious when the process is started. So here’s a way to detect such problems without needing to access source code:
Count the number of thread stacks in the process. That includes the number of running active threads and terminated threads.
Count the number of active running threads in the process.
Compare the two. If the number of the existing thread stacks is greater than the number of active running threads, and the dispersion of these two numbers keeps increasing as the program continues running, then memory is leaking.
Use /proc/PID/task to count active threads
[root@oceanbase task]# ls /proc/29363/task | wc -l
555
Use pmap to count thread stacks
cat /proc/29363/maps |grep stack |wc -l
555
----- 1 root root 64 Aug 14 17:19 1032 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1031 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1030 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1029 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1028 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1027 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1026 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1025 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1024 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1023 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1022 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1021 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1020 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1019 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1018 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1017 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1016 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1015 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1014 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1013 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1012 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1011 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1010 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1009 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1008 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1007 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1006 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1005 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1004 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1003 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1002 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 14 17:19 1001 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Aug 14 17:19 1000 -> anon_inode:[eventpoll]
[root@oceanbase fd]#
最终再《fs/proc: Stop trying to report thread stacks》将所有[stack:TID]全部移除。
那么在没有[stack:TID]的情况下如何断定vma是否是线程栈呢?
首先线程栈大小可以通过ulimit -s查看,所以maps中vma大小和这个一致;并且属性应该是匿名的rw-p。
然后上面应该是一页大小作为分隔区间,分隔页的属性应该是---p。
- END -
关注,回复【1024】海量Linux资料赠送
精彩文章合集