Thread safety測試
這裡先用一個小程式來測試mtrace在multithread系統是否可以準確的偵測出memory leak:
#include <unistd.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <mcheck.h> #define THREAD_COUNT 200 #define MALLOC_COUNT 3000 void* threadFunc(void* arg) { int i; unsigned size=0; void* ptr[MALLOC_COUNT]; FILE* pf = fopen("/dev/urandom", "r"); if (pf == NULL) { printf("cannot open /dev/urandom\n"); return NULL; } for (i=0; i<MALLOC_COUNT; i++) { if (fread(&size, sizeof(unsigned char), 1, pf) != sizeof(unsigned char)) { printf("fread fail\n"); size = 0; } ptr[i] = malloc(size+1); } sleep(20); for (i=0; i<MALLOC_COUNT; i++) free(ptr[i]); fclose(pf); return NULL; } int main( int argc, char* argv[]) { pthread_t tid[THREAD_COUNT]; int i; if (argc == 2) { setenv("MALLOC_TRACE", argv[1], 1); mtrace(); } for (i=0; i<THREAD_COUNT; i++) pthread_create(&tid[i], NULL, threadFunc, NULL); for (i=0; i<THREAD_COUNT; i++) pthread_join(tid[i], NULL); if (argc == 2) muntrace(); return 0; }在這個程式裡我們create出200個thread,每個thread各有3000次malloc-free pair,malloc的size則亂數分佈在0x1~0x100之間。
照邏輯看來這程式的malloc與free是成對的,也就是不會有memory leak,但實際測起來mtrace卻報了相當多的Memory not freed:
改用Valgrind來觀察同一個binary,如預期般的回報沒有發現leak。看來Valgrind在multithread系統中是比較值得信賴的!
接下來我們做另一個實驗,這次在每次呼叫free()的前後加mutex保護,確保一次只有一個thread能夠執行free():
for (i=0; i<MALLOC_COUNT; i++) { pthread_mutex_lock(&mtx); free(ptr[i]); pthread_mutex_unlock(&mtx); }再跑一次果然就沒再報出leak了。
從實驗結果看來,mtrace果然不適用於multithread system!在現實之中我們當然不可能為了使用mtrace而在原程式加一堆mutex,何況有些malloc與free是隱藏在library function之中的 (如pthread_create, c++的new operator, std container ... )。
mtrace原理
接下來我們試著深入研究mtrace的運作原理並找出造成false positive的原因。首先先看一下mtrace產生出來的bookkeeping檔案
這檔案記錄著程式裡所有malloc(以"+"表示)與free(以"-"表示)的資訊,包含分配到的動態空間位址(即malloc() return回來的值)、size及caller(讓我們可以知道是誰造成leak)。只要根據這些資訊去逐一比對確認所有malloc拿到的address都有對應的free,那我們就可以說程式是沒有memory leak的。
接著來研究mtrace是如何印出這些資訊的。既然我們是call mtrace()來啟動整個偵測flow的,理所當然的就從它開始看吧:
這個function做了兩件事
1. 讀出環境變數MALLOC_TRACE,然後fopen()它做為輸出檔案
2. 備份__malloc_hook (如果沒有特別去改它的話預設值應該是NULL),接著將tr_mallochook assign給__malloc_hook (__free_hook, __realloc_hook, __memalign_hook同理)
這裡的malloc_hook是GNU C library提供的一種redirect機制,讓我們可以把程式裡所有呼叫malloc()的function call redirect到我們自己定義的function。這功能允許我們很輕鬆的置換另一個memory allocate algorithm (例如dlmalloc),或是做一些bookkeeping之類的工作。
glibc的manual: 3.2.2.10 Memory Allocation Hooks裡有詳細的說明,不過直接看source code更清楚:
程式一執行到__libc_malloc()馬上就去讀__malloc_hook這個變數,如果它是NULL,那就照平常那樣動態分配一塊記憶體給上層application使用;如果它不是NULL,那就會被redirect去執行我們設定的__malloc_hook function,分配記憶體的工作將不再由原先的malloc()負責。
而在mtrace()裡我們看到malloc_hook被設為tr_mallochook(),亦即程式裡所有malloc()會全被redirect到tr_mallochook()。
那tr_mallochook()又做了什麼呢?
除了記錄之外這裡還必須做動態記憶體的分配與管理,別忘了malloc()已經被redirect到此,原本malloc()該做的分配動態記憶體還是要做,不然上層的應用程式是跑不下去的。
glibc在這裡使用了一種取巧的方法,tr_mallochook()再度回call malloc(),讓它繼續幫我們分配記憶體空間。但是直接回call會出問題,因為此時__malloc_hook已經被設為tr_mallochook(),所以一回call malloc()馬上就又被redirect到tr_mallochook(),然後就形成infinite loop。所以在回call malloc()之前必須還原__malloc_hook。這樣一來既可以做bookkeeping,也可再利用原始的malloc()來分配記憶體,可以說是一舉兩得啊!
整個flow可以粗略的用下圖來描述:
可惜這巧妙的設計來到multithread的世界就走了樣!
問題就出現tr_mallochook()裡面的還原與重設__hook_malloc之間 (即上圖中的3,4之間)。如果這時候恰好發生context switch到其他thread,而該thread又剛好call了malloc()...
可以想像的,由於__malloc_hook已經被還原了,所以此時其他thread再call malloc()是不會被redirect到tr_mallochook(),也就是我們就少記錄了這筆資料。不單是malloc(),所有透過hook機制的free(), realloc(), memalign()均有可能發生這種情況,於是就造成了明明有free,但因為漏記了而被誤判為leak的情形了。
可能的解決方案
知道原理後就可以試著思考solution了。由於malloc(), mtrace(), tr_mallochook()均在glibc裡,第一個方案就是直接修改它們。mtrace雖然是MT-Unsafe,但malloc(), free()這些function卻都是thread safe的,差別就在於它們有用mutex保護著:只是這個mutex是放在檢查__malloc_hook之後,也許我們可以試著把它拉到function最前面。不過mutex防守範圍愈大相對的也會降低程式的parallelism。原本的mutex只保護少數幾行關鍵global variable的修改,但現在卻拉大到整個function,效能降低應該蠻明顯的。
第二種修改方式是不去動glibc,仿照mtrace的模式用__malloc_hook自己來兜。
這做法彈性就大的多,我們可以使用dlmalloc來幫我們分配記憶體,這樣一來就不用不斷的還原與重設__malloc_hook,自然也不會發生synchronization的問題。概念上就如下圖表示:
另外在這種模式下我們還以印出更多資訊,像是thread id、thread name, 甚至還能透過backtrace(3)印出完整的call frame,這樣在分析memory leak上就更加得心應手了!
沒有留言:
張貼留言