基於哈希表的內存泄漏檢測方法

    在C++程序中,內存問題除了非法改寫,還有另外一個很重要也很頻繁出現的問題是堆內存未釋放。若是在高負載網絡應用中,出現這個問題,很快會致使服務崩潰。之前檢測此類問題的辦法是在每個內存分配和釋放處加上log,而後人肉debug,可是……面對幾十萬行內存分配/釋放trace,相信大多數人會喪失查找問題所在的勇氣更別說高效率解決問題了。html

     所幸已經有高人大賢包裝了基於內存分配器的跟蹤器,以管理內存塊生命期的方式來定位問題(參見http://www.cnblogs.com/clover-toeic/p/3819636.html)。可是原文中使用的數據結構是鏈表,這樣在刪除內存管理結構時顯然會帶來性能問題,並且該方法仍是線程不安全的。有鑑於此,我作出了對應性改進,主要是用C++11中的unordered_map(基於hash table)取代了鏈表,顯著提升了內存塊釋放時的查找速度;另外就是編寫了一個基於TLS的包裝類,將內存分配跟蹤器per thread化,這樣就能夠用於多線程程序中。安全

     首先咱們定義一個內存塊管理結構:網絡

typedef struct mem_info {
  const char* fileName;
  const char* funcName;
  uint32_t codeLine;
  pid_t tid;
  size_t memSize;
  void* memAddr;
} mem_info_t;

因爲咱們須要直接刪除內存塊,那麼它在hash table中的存放方式,就應該是{void* ptr, mem_info_t* mi},這樣的一個pair做爲unordered_map的元素。同時還要記錄總共分配了多少字節的內存,以及總共的分配次數,所以採用一個稱爲global_meminfo的類來完成這一任務: 數據結構

class global_meminfo {
public:
  global_meminfo(): allocBytes_(0), allocTimes_(0), releaseBytes_(0), releaseTimes_(0) {}多線程

 
 

  void saveMemInfo(void* ptr, mem_info_t* mi) {
    uint64_t addr = reinterpret_cast<uint64_t>(ptr);
    tracked_meminfo_.insert({addr, mi});
    ++allocTimes_;
    allocBytes_ += mi->memSize;
  }函數

 
 

  void removeMemInfo(void* ptr) {
    uint64_t addr = reinterpret_cast<uint64_t>(ptr);
    auto ele = tracked_meminfo_.find(addr);
    if ( ele == tracked_meminfo_.end() ) {
      printf("No valid memory block found(%p)\n", ptr);
      return;
    }
    ++releaseTimes_;
    releaseBytes_ += ele->second->memSize;
    free(ele->second->memAddr);
    free(ele->second); // release mem_info_t*
    tracked_meminfo_.erase(ele);
  }性能

 
 

  ~global_meminfo() {
    printf("Total memory allocated: %zu \n", allocBytes_);
    printf("Times of memory allocation: %u\n", allocTimes_);
    printf("Total memory released: %zu\n", releaseBytes_);
    printf("Times of memory releasing: %u\n", releaseTimes_);
    size_t unreleased = 0;
    if ( !tracked_meminfo_.empty()) {
      for ( auto& ele: tracked_meminfo_ ) {
        unreleased += ele.second->memSize;
        free(ele.second->memAddr);
        free(ele.second);
      }
    }
    printf("Thread %s: Unleased memory: %zu of %zu bytes\n", \
    CurrentThread::getTidString(), tracked_meminfo_.size(), unreleased);
    tracked_meminfo_.clear();
}ui

 
 

private:
  typedef unordered_map<uint64_t, mem_info_t*> tracked_mem_info_t;
  tracked_mem_info_t tracked_meminfo_;

  size_t allocBytes_;
  uint32_t allocTimes_;
  size_t releaseBytes_;
  uint32_t releaseTimes_;
};spa

 

未釋放的內存塊信息會在該對象析構時打印出來。有了內存分配跟蹤器的管理類,那麼如何將其per thread化?在Linux中提供了pthread_getspecific來實現TLS。那麼能夠採用一個模板類來包裝之:線程

template<typename T>
class ThreadLocalStorage: public Noncopyable {
public:
  ThreadLocalStorage() {
    pthread_key_create(&pKey_, &ThreadLocalStorage::destroyer);
  }

  ~ThreadLocalStorage() {
    pthread_key_delete(pKey_);
  }

  T& value() {
    T* v = static_cast<T*>(pthread_getspecific(pKey_));
    if ( !v ) {
      T* newObj(new T);
      pthread_setspecific(pKey_, newObj);
      v = newObj;
    }

    return *v;
  }

private:
  static void destroyer(void* x) {
    T* v = static_cast<T*>(x);
    typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1: 1];
    T_must_be_complete_type foo; (void)foo;
    delete v;
  }

  pthread_key_t pKey_;
};

這樣就能夠將全局的global_meminfo對象存儲到TLS中了:

ThreadLocalStorage<global_meminfo> g_meminfo;

有了內存分配跟蹤器的包裝類以後,再來從新定義內存分配和釋放函數:

void* tracked_malloc(size_t size, const char* file, const char* func, uint32_t line) {
  void* ptr = malloc(size);
  mem_info_t* mi = new mem_info_t;
  mi->fileName = file;
  mi->funcName = func;
  mi->codeLine = line;
  mi->tid = CurrentThread::getTid();
  mi->memSize = size;
  mi->memAddr = ptr;
  global_meminfo& mem_info = g_meminfo.value(); 
  mem_info.saveMemInfo(ptr, mi);

  return ptr;
}

void tracked_free(void* ptr) {
  global_meminfo& mem_info = g_meminfo.value();
  mem_info.removeMemInfo(ptr);
}

#define TRACKED_MALLOC(size) tracked_malloc(size, __FILE__, __FUNCTION__, __LINE__)
#define TRACKED_FREE(ptr)    tracked_free(ptr)
tracked_malloc的功能很簡單,將分配出的內存地址/大小,所在文件/行數/函數名及當前線程ID保存至hash table。固然這兩個宏只適用於C語言程序,對於C++,由於operator new也是基於malloc/free的,因此只要繼續定義一個本身的operator new取而代之便可。
最後咱們經過一個簡單的demo程序來演示下這個方案的能力:
void func() {
  for ( int i = 0; i < 1024; ++i ) {
    void* ptr = TRACKED_MALLOC(8);
    if ( i < 512 )
      TRACKED_FREE(ptr);
  }
}

int main() {
  std::thread t1(func);
  std::thread t2(func);
  t1.join();
  t2.join();
  sleep(5);

  return 0;
}

兩個線程獨立運行,8個字節的內存分配1024次可是隻釋放512次。所以會泄漏4096字節,是否如此呢?實際運行一下就知道了:

Total memory allocated: 8192 
Times of memory allocation: 1024
Total memory released: 4096
Times of memory releasing: 512
Thread 1504: Unleased memory: 512 of 4096 bytes
Total memory allocated: 8192 
Times of memory allocation: 1024
Total memory released: 4096
Times of memory releasing: 512
Thread 1503: Unleased memory: 512 of 4096 bytes

可見是可以檢測到各線程的內存泄漏狀況的。

相關文章
相關標籤/搜索