日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

mmap函数_分析由 mmap 导致的内存泄漏

發布時間:2024/9/27 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 mmap函数_分析由 mmap 导致的内存泄漏 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

背景

一個程序鏈接 TCMalloc ,同時調用 mmap / munmap 管理一部分較大的內存

通過 TCMalloc 的統計信息,判斷內存泄漏不是由 new / malloc 等常規接口導致的

因此懷疑是 mmap 導致的內存泄漏

hook

hook mmap / munmap 記錄下每一次調用,可以分析出是哪部分導致的內存泄漏

如何存儲調用信息?

這涉及到三個問題的回答:

  • buffer 是 thread local 還是 global ?
  • 如何處理 buffer 滿的情況?
  • 什么時候將 buffer 寫出?
  • 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 滿的時候同步寫出
  • 另起一個線程寫出
  • 以 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();}

    如何獲取調用棧?

  • libunwind 提供的 backtrace 函數
  • glibc 指代的 backtrace 函數
  • 獲取 rsp / rbp 手動遍歷
  • __builtin_frame_address
  • 第 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 ()

    可以看到:

  • libunwind 將 glibc 提供的 backtrace 換成了自己的實現
  • _ULx86_64_step 會調用 mmap 函數
  • 為了避免死鎖,我們要用一個 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 ,可以看到:

  • hook 后 mmap / munmap 的耗時大概是 hook 前的 35 倍
  • hook 后變慢程度并沒有隨著線程的增長而增長
  • g++ -std=c++11 mmap.cpp ringed_buffer.cpp -ltcmalloc -lunwind -lpthread -ldl -O3 -ggdb -shared -fPIC -o libmmap_analyser.so g++ -std=c++11 test.cpp -lpthread -ltcmalloc -lunwind -O3 -ggdb -o test time ./test time env LD_PRELOAD="libmmap_analyser.so" test 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的mmap函数_分析由 mmap 导致的内存泄漏的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。