目前線上代碼有一定的內存洩漏問題,大多數情況下這種bug都難以追蹤定位,因此想開發一個內存監測小工具。
需要兩種監測方式。一種是全局監測,紀錄每一次內存的分配和釋放活動;另一種是較為輕量級的監測,只監測部分疑似存在洩漏的code。
內存監測需要hack進內存分配和釋放相關的代碼,監測其每次的活動。
首先想到的是對管理動態分配內存的函數進行改寫和監測。而new 和 delete很容易重寫,且據說也是線上用的最多的動態分配內存方式。
重載 ::operator new() 的理由Effective C++ 第三版第 50 條列舉了定制 new/delete 的幾點理由:
然而,這麼做的缺點也有很多,參見《不要重載全局new》http://www.360doc.com/content/12/1211/17/9200790_253442412.shtml 主要問題是與c庫的兼容問題
既然不選擇重載new/delete,就只能從malloc free入手了。且new delete的底層也是由malloc free實現的。這裡大約由如下幾種
優點:方便
缺點:不能替換系統函數使用到的malloc,如 sprintf new 等使用的還是系統malloc
缺點:有很多函數,影響整個系統,不只是當前代碼使用
優點:只要你在程序中寫上”__malloc_hook = my_malloc_hook;”,之後的malloc調用都會使用my_malloc_hook函數,方便易行。
缺點:但是這組調試變量不是線程安全的,當你想用系統malloc的時候不得不把他們改回來,多線程調用就得上鎖了。因此方案不很適用於系統內存優化,勉強用來簡單管理線程內存使用。
詳細用法:http://linux.die.net/man/3/__malloc_hook
/* Prototypes for our hooks. */ static void my_init_hook(void); static void *my_malloc_hook(size_t, const void *); /* Variables to save original hooks. */ static void *(*old_malloc_hook)(size_t, const void *); /* Override initializing hook from the C library. */ void (*__malloc_initialize_hook) (void) = my_init_hook; static void my_init_hook(void) { old_malloc_hook = __malloc_hook; __malloc_hook = my_malloc_hook; } static void * my_malloc_hook(size_t size, const void *caller) { void *result; /* Restore all old hooks */ __malloc_hook = old_malloc_hook; /* Call recursively */ result = malloc(size); /* Save underlying hooks */ old_malloc_hook = __malloc_hook; /* printf() might call malloc(), so protect it too. */ printf("malloc(%u) called from %p returns %p\n", (unsigned int) size, caller, result); /* Restore our own hooks */ __malloc_hook = my_malloc_hook; return result; }
調用dlsym來對系統malloc函數進行存取,保存為sys_malloc。然後就可以重寫malloc了,在重寫的malloc裡面調用sys_malloc來進行真正的內存分配。
詳細用法及注意事項:http://www.slideshare.net/tetsu.koba/tips-of-malloc-free
功能: 根據動態鏈接庫操作句柄與符號,返回符號對應的地址。 包含頭文件: #include函數定義: void* dlsym(void* handle,const char*symbol) 函數描述: 根據 動態鏈接庫 操作句柄(handle)與符號(symbol),返回符號對應的地址。使用這個函數不但可以獲取函數地址,也可以獲取變量地址。 handle:由dlopen打開動態鏈接庫後返回的指針; symbol:要求獲取的函數或全局變量的名稱。 返回值: void* 指向函數的地址,供調用使用。
extern "C" void* malloc(size_t size) { void * ptr=sys_malloc(size); handle extra behavior... return ptr; }
使用sys_malloc來進行真正的內存分配,然後做一些記錄等額外的操作,最後返回指針。
static bool performance_enabled_ = false; static void* (*sys_malloc)(size_t) = 0; static void* (*sys_realloc)(void*,size_t) = 0; static void* (*sys_calloc)(size_t,size_t) = 0; static void (*sys_free)(void*) = 0; static void initialize_functions() { sys_malloc = reinterpret_cast(dlsym(RTLD_NEXT, "malloc")); sys_realloc = reinterpret_cast (dlsym(RTLD_NEXT, "realloc")); sys_calloc = reinterpret_cast (dlsym(RTLD_NEXT, "calloc")); sys_free = reinterpret_cast (dlsym(RTLD_NEXT, "free")); }
進程可以使用dlsym(3C)獲取特定符號的地址。此函數采用句柄和符號名稱,並將符號地址返回給調用方。特殊句柄RTLD_NEXT允許從調用方鏈接映射列表中的下一個關聯目標文件獲取符號。由於我們重寫了malloc,RTLD_NEXT就指向了系統的malloc。這樣,sys_malloc,sys_realloc,sys_calloc,sys_free則存儲了系統的內存管理函數。
上面兩條描述了malloc重寫的核心操作。我們可以在第一次使用malloc的時候對sys_malloc進行初始化。
extern "C" void* malloc(size_t size) { if(sys_malloc==0) initialize_functions(); void * ptr=sys_malloc(size); handle extra behavior... return ptr; }
然而,調用dlsym來對系統malloc函數進行存取的時候,會調用calloc,如果calloc也用dlsym進行重載了,會造成循環調用。在你調用dlsym這個函數時,dlsym會調用dlerror,dlerror會調用calloc,calloc要調用malloc,而你的malloc正在初始化等待dlsym返回中,於是死循環了。
因此在第一次調用malloc或者calloc的時候可以分配一段靜態的內存供dlsym使用以解決這個問題。
char tmpbuff[1024]; unsigned long tmppos = 0; unsigned long tmpallocs = 0; extern "C" void* malloc(size_t size) { static bool is_initializing = false; if(sys_malloc == 0) { if(!is_initializing) { is_initializing = true; initialize_functions(); is_initializing = false; } else { if(tmppos+size
當extra behavior中涉及到動態內存分配和釋放的行為時,就會出現循環調用。 extra behavior->malloc->extra behavior->malloc->....因此我們需要使用一個flag來標志需要記錄的外部變量。通過此flag來判斷是否進行額外操作。這裡使用了thread local storage的標志 __thread, 用於標識每個線程的is_external情況。
static __thread bool is_external = true; extern "C" void* malloc(size_t size) { static bool is_initializing = false; if(sys_malloc == 0) { if(!is_initializing) { is_initializing = true; initialize_functions(); is_initializing = false; } else { if(tmppos+size
使用intbacktrace(void**buffer,intsize) 函數得到當前線程的調用堆棧。
使用 size_t malloc_usable_size (void *ptr) 函數獲得該地址指針指向的內存大小。
note: malloc(size) calloc(size,cnt) realloc(ptr,size)等函數分配的內存大小不一定是調用函數的時候賦予的size的大小,主要是由於地址對齊的考慮。所以需要用malloc_usable_size來獲得真正的內存大小。
對malloc,calloc,realloc,free使用dlsym來對系統函數進行存取的方式進行重寫。使得內存分配函數除了分配內存還能做一些記錄內存分配的操作。
monitor主要數據結構如下
//record general performance info struct General_Performance { int64_t used_memory_; std::vectorstack_trace_; }; //record general performance info by address struct General_Performance_Map: public std::unordered_map { }; //record specific performance info struct Specific_Performance { size_t count_; int64_t duration_; //mili seconds int64_t hold_memory_; //the memory change size during a monitor life time int64_t allocated_memory_;//the memory allocation size during a monitor life time int64_t start_time_; //mili seconds int64_t end_time_; //mili seconds General_Performance_Map detail_; }; //record specific performance info by guard's tag struct Specific_Performance_Map: public std::unordered_map { }; //record the specific & general info for each thread struct Thread_Performance_Info { int guards_num_; //the number of guards in current thread General_Performance_Map general_performance_map_;//record the general performance by address std::list stack_guard_father_call_names_;//record the father function called Guard as a stack std::list stack_specific_performance_;//record the specific performance as a stack, each only manipulate the back element, Specific_Performance_Map specific_performance_map_;//record the specific performance by guard's tag Thread_Performance_Info(); ~Thread_Performance_Info();//do merge work when destruct a thread info }; //when a guard object exist, the corresponding specific monitor will be at working status struct Guard { Ads_String tag_; int64_t start_time_; int64_t end_time_; Guard(const Ads_String& tag); ~Guard(); };
對每一次內存的分配和釋放都進行記錄,並使用backtrace進行調用跟蹤,獲得全局的內存分配信息,需要通過編譯選項打開.
1.1在每個線程創建一個unordered_map
1.2每次內存操作的時候更新該線程的map信息
1.3線程退出時,把當前的map信息merge到全局的general info map中
使用宏定義,展開成構造一個Guard,在Guard的對象的作用域內統計分配內存大小,起止時間,進入次數等信息。利用函數的棧調用形式,Guard可以使用棧結構存儲,只操作棧頂元素,在需要pop時把信息merge到新的棧頂
2.1通過PERFORMANCE_MONITOR的宏展開成為函數名的guard結構體,並保存當前的時間信息入棧stack_specific_performance
2.2每次內存操作查看stack_specific_performance是否為空,不為空則記錄內存的變化信息以及call stack到棧頂
2.3guard結構體生命周期結束析構的時候,記錄當前時間,計算duration,start time,end time等信息,並彈出棧頂。如果棧不為空,則把當前的specific info 合並到新的棧頂元素。
工具的使用環境是多線程的,如果每次內存信息的記錄都merge到全局的變量,必然要給變量加鎖以免沖突,這會極大的降低效率。 因此我們需要構造記錄監測信息的線程內變量,在線程退出的時候,把這些變量merge到全局變量中。這樣可以極大的減少鎖的使用。
因此可以使用__thread_local Thread_Performance_Info>的形式構造線程變量,並且在Thread_Performance_Info的析構函數中加入線程變量的merge行為。因為TSS變量在析構的時候,會調用類型的析構函數。這樣就滿足了在線程析構的時候才merge到全局變量的要求。