程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 編寫內存洩露檢測器的方法選擇以及實現方式 c++

編寫內存洩露檢測器的方法選擇以及實現方式 c++

編輯:C++入門知識

編寫內存洩露檢測器的方法選擇以及實現方式 c++


目的

目前線上代碼有一定的內存洩漏問題,大多數情況下這種bug都難以追蹤定位,因此想開發一個內存監測小工具。
需要兩種監測方式。一種是全局監測,紀錄每一次內存的分配和釋放活動;另一種是較為輕量級的監測,只監測部分疑似存在洩漏的code。

內存監測需要hack進內存分配和釋放相關的代碼,監測其每次的活動。

方法選擇

1.重載new/delete

首先想到的是對管理動態分配內存的函數進行改寫和監測。而new 和 delete很容易重寫,且據說也是線上用的最多的動態分配內存方式。

重載 ::operator new() 的理由Effective C++ 第三版第 50 條列舉了定制 new/delete 的幾點理由:

    • 檢測代碼中的內存錯誤
    • 優化性能
    • 獲得內存使用的統計數據

      然而,這麼做的缺點也有很多,參見《不要重載全局new》http://www.360doc.com/content/12/1211/17/9200790_253442412.shtml 主要問題是與c庫的兼容問題

      2.從malloc,free入手

      既然不選擇重載new/delete,就只能從malloc free入手了。且new delete的底層也是由malloc free實現的。這裡大約由如下幾種

      1.#define malloc(x ) debug_malloc(x,__file__,__line__)

      優點:方便

      缺點:不能替換系統函數使用到的malloc,如 sprintf new 等使用的還是系統malloc

      2.修改glibc或(libc.so)

      缺點:有很多函數,影響整個系統,不只是當前代碼使用

      3.__malloc_hook

      優點:只要你在程序中寫上”__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;
      }

       

      4.dlysm

      調用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* 指向函數的地址,供調用使用。

       

       

      使用DLYSM重寫系統的內存管理函數

      1.函數原型設計

      extern "C" void* malloc(size_t size)
      {
      	void * ptr=sys_malloc(size);
      	handle extra behavior...
      	return ptr;
      }

      使用sys_malloc來進行真正的內存分配,然後做一些記錄等額外的操作,最後返回指針。

      2.存儲系統真正內存分配釋放函數

      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則存儲了系統的內存管理函數。

      3.初始化問題

      上面兩條描述了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
      

      4.循環調用的問題

      當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
      

      5.輔助函數

      使用intbacktrace(void**buffer,intsize) 函數得到當前線程的調用堆棧。

      使用 size_t malloc_usable_size (void *ptr) 函數獲得該地址指針指向的內存大小。

      note: malloc(size) calloc(size,cnt) realloc(ptr,size)等函數分配的內存大小不一定是調用函數的時候賦予的size的大小,主要是由於地址對齊的考慮。所以需要用malloc_usable_size來獲得真正的內存大小。

      Monitor 設計

      對malloc,calloc,realloc,free使用dlsym來對系統函數進行存取的方式進行重寫。使得內存分配函數除了分配內存還能做一些記錄內存分配的操作。

      monitor主要數據結構如下

      	//record general performance info
       	struct General_Performance
          {
              int64_t used_memory_;
              std::vector stack_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();
          };
      
      

       

      1.全局的memory leak detection

      對每一次內存的分配和釋放都進行記錄,並使用backtrace進行調用跟蹤,獲得全局的內存分配信息,需要通過編譯選項打開.

      1.1在每個線程創建一個unordered_map ,這裡key是用於內存分配的指針,value是這個指針的當前大小以及導致這次內存分配/釋放的call stack

      1.2每次內存操作的時候更新該線程的map信息

      1.3線程退出時,把當前的map信息merge到全局的general info map中

       

      2.specific的memory leak detection

      使用宏定義,展開成構造一個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 合並到新的棧頂元素。

       

      3.性能優化

      3.1多線程優化

      工具的使用環境是多線程的,如果每次內存信息的記錄都merge到全局的變量,必然要給變量加鎖以免沖突,這會極大的降低效率。 因此我們需要構造記錄監測信息的線程內變量,在線程退出的時候,把這些變量merge到全局變量中。這樣可以極大的減少鎖的使用。

      因此可以使用__thread_local Thread_Performance_Info>的形式構造線程變量,並且在Thread_Performance_Info的析構函數中加入線程變量的merge行為。因為TSS變量在析構的時候,會調用類型的析構函數。這樣就滿足了在線程析構的時候才merge到全局變量的要求。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved