在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
可見是可以檢測到各線程的內存泄漏狀況的。