mmap函数_分析由 mmap 导致的内存泄漏
背景
一個程序鏈接 TCMalloc ,同時調用 mmap / munmap 管理一部分較大的內存
通過 TCMalloc 的統計信息,判斷內存泄漏不是由 new / malloc 等常規接口導致的
因此懷疑是 mmap 導致的內存泄漏
hook
hook mmap / munmap 記錄下每一次調用,可以分析出是哪部分導致的內存泄漏
如何存儲調用信息?
這涉及到三個問題的回答:
thread local / global
thread local 的優勢是不需要任何同步手段,劣勢是時序關系無法保證
內存的分配與釋放未必是同一個線程,如果多線程之間 mmap / munmap 的時序關系沒有記錄下來,后期很難恢復,也很難知道是哪個線程導致的泄漏
global buffer 的劣勢是需要同步手段,同步手段可以選擇原子變量(比鎖輕)
// 1. 用原子變量搶寫入空間 uint64_t index = mEndIndex.fetch_add(2, std::memory_order_relaxed); mBuffer[index] = GenFirstValue(Type::eMunmap, cycle, p); // 2. 寫入 mBuffer[index + 1] = GenSecondValue(isSucceed, munmapSize);一旦將寫入位置定下來,不同線程的寫入并不會發生沖突
fetch_add 注意用最松的 memory order 來保證性能受到最低限度的影響
如何處理 buffer 滿的情況?
三種處理手段:不寫入、扔掉前面的信息、等待 buffer 刷新
等待 buffer 刷新不可避免地引入 PV 等同步手段(生產者、消費者模型),這會導致性能受到的影響不可控
不寫入和扔掉前面的信息本質上是同一種處理手段,在無法判斷信息重要性的前提下,兩者任意選一種皆可
最終選擇扔掉前面的信息,理由如下:
如果發生信息覆蓋,需要留下標記,方便分析(至少可以提示用戶)
引入長度為 2 bits 的 cycle 字段,cycle = the lowest 2 bits of (index / buffer size)
*cycle = (index / mBufferSize) & 0x3;將 cycle 字段寫出到 buffer ,當分析程序看到 cycle 變化較快的時候,就知道出現了信息丟棄的情況
什么時候將 buffer 寫出?
以 buffer 滿作為寫出條件會導致一個問題:如何處理 buffer 未滿的情況?如果一個程序 mmap / munmap 的次數較少,記錄不足以寫滿 buffer ,那么 buffer 只能在進程結束的時候通過全局變量的析構函數一次性寫出。但不是所有的程序都是 gracefully shutdown 的,特別是某些因為內存超限被 OOM Killer 殺掉的程序,這些程序的析構函數未必有機會得到調用。
另外,異步寫出與寫入 buffer 有競爭關系,可能導致數據混亂
另起一個線程寫出有一個比較坑的地方:不要調用 std::thread 或者 pthread_create 來啟動一個線程
因為我們的動態鏈接庫是很早加載的(這樣才能 hook mmap / munmap),此時 libpthread.so 還沒有加載進來,直接調用函數會導致異常
mPThreadLib = dlopen("libpthread.so", RTLD_LAZY | RTLD_LOCAL); // 啟動線程 using FuncType = void* (*)(void*); using PThreadCreateType =int (*)(pthread_t*, pthread_attr_t*, FuncType, void*); auto pthreadCreate = reinterpret_cast<PThreadCreateType>(dlsym(mPThreadLib, "pthread_create")); auto pf = &RingedBuffer::Dump; pthreadCreate(&mDumpThread, nullptr, *reinterpret_cast<FuncType*>(&pf), this); // 停止線程 using PThreadJoinType = int (*)(pthread_t, void**); auto pthreadJoin = reinterpret_cast<PThreadJoinType>(dlsym(mPThreadLib, "pthread_join")); void* ret = nullptr; pthreadJoin(mDumpThread, &ret);全局對象初始化順序
我們有一個全局變量 RingedBuffer sRingedBuffer 負責記錄調用信息,我們能否依賴構造函數將其成員變量初始化?
要注意:mmap / munmap 并不是只有 main 函數才會調用,TCMalloc / pthread 都會調用這兩個函數
即使我們的動態鏈接庫先于這兩個庫加載,也沒有辦法保證 sRingedBuffer 的構造函數先于 TCMalloc / pthread 的全局變量調用
因此,需要在每一次記錄之前都調用一下 Init 函數
void RecordMmap(void* p, int mmapSize, char** funcNames, int funcNamesSize) {Init();// Do other thing. }TCMalloc 中也采用了相同的做法:
void* do_memalign(size_t align, size_t size) {if (Static::pageheap() == NULL) ThreadCache::InitModule();}如何獲取調用棧?
第 3 種和第 4 種方法都會在開優化編譯過的程序上面臨 coredump 風險,因為棧底指針的壓棧不再是必須的
uint64_t* rbp; asm("mov %%rbp,%0" : "=r"(rbp)); auto ra = *(rbp + 1);以上代碼在遍歷深度不為 1 的時候會碰到 coredump 問題
libunwind 能幫我們處理掉這些 tricky 的角落,用 libunwind 是不錯的選擇
libunwind 的一些函數使用了不可重入鎖,并且關了終端,所以不做特殊處理的話,會看到程序無法用 Ctrl-C 殺死,只能用 kill -9 結束
#0 0x00007f7e5119653d in __lll_lock_wait () #1 0x00007f7e51191e1b in _L_lock_883 () #2 0x00007f7e51191ce8 in pthread_mutex_lock () #3 0x00007f7e513a8aca in ?? () #4 0x00007f7e513a91f9 in ?? () #5 0x00007f7e513ab206 in _ULx86_64_step () #6 0x00007f7e513a6576 in backtrace () #7 0x00007f7e5182fc9f in mmap (addr=0x0, length=4096, prot=3, flags=34, fd=-1, offset=0) #8 0x00007f7e513a937d in ?? () #9 0x00007f7e513a9c5b in ?? () #10 0x00007f7e506d749c in dl_iterate_phdr () #11 0x00007f7e513aa23e in ?? () #12 0x00007f7e513a7c2d in ?? () #13 0x00007f7e513a8d72 in ?? () #14 0x00007f7e513a91f9 in ?? () #15 0x00007f7e513ab206 in _ULx86_64_step () #16 0x00007f7e513a6576 in backtrace () #17 0x00007f7e5182fc9f in mmap (addr=0x0, length=4096, prot=3, flags=34, fd=-1, offset=0) #18 0x00000000004011dd in main ()可以看到:
為了避免死鎖,我們要用一個 thread local 變量記錄 libunwind 提供的函數是否已經被調用了
// Initializer::Init 負責用 dlopen 和 dlsym 加載 _ULx86_64_init_local 和 _ULx86_64_stepint _ULx86_64_init_local(unw_cursor_t* cursor, unw_context_t* context) {// Prevent sUnwInitLocal is nullptr if static vars of tcmalloc// is initialized before mmap.Initializer::Init();tBacktracing = true;auto r = Initializer::sUnwInitLocal(cursor, context);tBacktracing = false;return r; }int _ULx86_64_step(unw_cursor_t* cursor) {// Prevent sUnwStep is nullptr if static vars of tcmalloc// is initialized before mmap.Initializer::Init();tBacktracing = true;auto r = Initializer::sUnwStep(cursor);tBacktracing = false;return r; }僅僅 hook 這兩個函數是不夠的,因為 libunwind 提供的 backtrace 函數在編譯時可以看見 _ULx86_64_init_local 和 _ULx86_64_step ,不會動態加載這兩個函數
所以還需要 hook backtrace 函數
int backtrace(void** returnAddrs, int skipCount, int maxDepth) {void* ip = nullptr;unw_cursor_t cursor;unw_context_t uc;unw_getcontext(&uc);int ret = unw_init_local(&cursor, &uc);assert(ret >= 0);// Do not include current frame.for (int i = 0; i < skipCount + 1; i++) {if (unw_step(&cursor) <= 0) {return 0;}}int n = 0;while (n < maxDepth) {if (unw_get_reg(&cursor, UNW_REG_IP, reinterpret_cast<unw_word_t*>(&ip)) < 0) {break;}returnAddrs[n] = ip;n++;if (unw_step(&cursor) <= 0) {break;}}return n; }backtrace 函數的實現可以借鑒 TCMalloc 的 GET_STACK_TRACE_OR_FRAMES 函數
如何將返回地址解釋成符號?
這里要做一個選擇:原地解釋還是事后解釋?
一般來說,事后解釋優勢很明顯:性能好
但是,有一些程序會反復調用 dlopen 和 dlclose ,這個時候事后解釋就會面臨信息不全的問題
補充一個冷知識:如果不考慮 dlopen 和 dlclose ,每一次進程啟動,庫加載到虛擬內存的位置是固定的
再補充一個冷知識:addr2line 2.27 有 bug ,解釋結果可能和 gdb 不一致
所以這個版本用了原地解釋的方案
void* returnAddrs[10]; int n = backtrace(reinterpret_cast<void**>(&returnAddrs), 1, 10); char** funcNames = backtrace_symbols(returnAddrs, n); // This array is malloced by backtrace_symbols(), and must be freed by the caller. (The strings pointed to by the array of pointers need not and should not be freed.) free(funcNames);boost 用了一種更加折中的方案:開一個子進程來解釋(這在理論上也會有 gap )
事后解釋具有實現的可能性:RTLD-AUDIT 能夠審計動態鏈接庫的加載與卸載,這會放在下一篇文章講
性能分析
單線程下的火焰圖(編譯時未開優化)
RecordMmap 在單線程下的表項并不算優異,經過分析,主要是字符串拷貝等操作消耗了很多時間
每個線程分別調用 10000 次 mmap 和 munmap ,可以看到:
總結
以上是生活随笔為你收集整理的mmap函数_分析由 mmap 导致的内存泄漏的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: mysql基础和高级整理_mysql基础
- 下一篇: 明装暖气怎么装?明装装暖气片需要几天?
