記錄一個關于線程內存泄漏問題的定位過程,以及過程中的收獲。
1. 初步定位
是否存在內存泄漏:想到內存泄漏,首先查看/proc/meminfo,通過/proc/meminfo可以看出總體內存在下降。確定內存泄漏確實存在。top中可以顯示多種形式內存,進而可以判斷是那種泄漏。比如vss/rss/pss等。
確定哪個進程內存泄漏:通過top即可查看到是哪個進程在泄漏。至此基本可以確定到哪個進程。
確定進程泄漏內存類型:然后查看進程的/proc//maps,通過maps可以看出泄漏的內存類型(堆、棧、匿名內存等等),有時候運氣好可以直接判斷泄漏點。
如果是slab:可以通過/proc/slabinfo,可以看出進程的動態變化情況。如果確定是哪一個slab,那么可以在/sys/kernel/slab//alloc_calls和free_calls中直接找到調用點。當然看到的是內核空間的函數。
使用mcheck():可以檢查malloc/free造成的泄漏問題。
通過如下腳本,然后對每次抓取內容進行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//task | wc -l確定進程下線程數目。然后在maps中檢查[stack:TID]數目。兩者如果不一致,則存在Joinable線程沒有調用pthread_join()造成的泄漏。
如果maps沒有[stack:TID],可以通過pmap | grep | wc -l,即通過檢查棧大小的vma數目來確定棧數目。
3. 問題根源
通過檢查線程棧消耗與實際線程數目,發現兩者數目吻合。所以線程并沒有退出。也即不是由于未使用pthread_join()導致的內存泄漏。
然后根據maps中[stack:TID]的pid號,cat /proc//comm發現是同一個線程不停創建。但是沒有釋放。
其實通過top -H -p 和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 createdn", 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 createdn", 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//maps》中,添加了[stack:TID]用于表示此vma對應的是線程TID的stack區域。
這樣做的好處是,可以從maps中明確知道此段vma是被哪個線程使用的。
有一個壞處就是先線程非常多情況下,主線程中為了顯示[stack:TIS],開銷就會很大,而實際上用處不是很大。
所以在《proc: revert /proc//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。
-
嵌入式
+關注
關注
5072文章
19026瀏覽量
303532 -
函數
+關注
關注
3文章
4308瀏覽量
62449 -
線程
+關注
關注
0文章
504瀏覽量
19653 -
內存泄漏
+關注
關注
0文章
39瀏覽量
9206
發布評論請先 登錄
相關推薦
評論