小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

程序員的福音,C/C++內(nèi)存泄漏的終極解決方案

 ekylin 2019-09-28

用C/C++開發(fā)的程序執(zhí)行效率很高,但卻難以駕馭,稍不留神就翻車,每個C/C++程序員都遭受過內(nèi)存泄漏的困擾。本文提供一種通過wrap malloc查找memory leak的思路,使得你翻車的時候能夠自救,而不至于車毀人亡。

程序員的福音,C/C++內(nèi)存泄漏的終極解決方案

什么是內(nèi)存泄漏?

動態(tài)申請的內(nèi)存丟失引用,造成沒有辦法回收它(我知道杠精要說進程退出前系統(tǒng)會統(tǒng)一回收),這便是內(nèi)存泄漏,內(nèi)存泄漏對于客戶端應(yīng)用可能不是什么大事,而對于長久運行的服務(wù)器程序則是致命的。

Java等編程語言會自動管理內(nèi)存回收,而C/C++需要顯式的釋放,有很多手段可以避免內(nèi)存泄漏,比如RAII,比如智能指針(大多基于引用計數(shù)計數(shù)),比如內(nèi)存池。

程序員的福音,C/C++內(nèi)存泄漏的終極解決方案

理論上,只要我們足夠小心,在每次申請的時候,都牢記釋放,那這個世界就清凈了,但現(xiàn)實往往沒有那么美好,比如拋異常了,釋放內(nèi)存的語句執(zhí)行不到,又或者某菜鳥不小心埋了顆雷,所以,我們必須直面真實的世界,那就是我們會遭遇內(nèi)存泄漏。

怎么查內(nèi)存泄漏?

我們可以review代碼,但從海量代碼里找到隱藏的問題,這如同大海撈針,往往兩手空空。

所以,我們需要借助工具,比如valgrind,但這些找內(nèi)存泄漏的工具,往往對你使用動態(tài)內(nèi)存的方式有某種期待,或者說約束。比如常駐內(nèi)存的對象會被誤報出來,然后真正有用的信息會掩蓋在誤報的汪洋大海里。很多時候,valgrind碰到現(xiàn)實項目便武功盡廢,幫不上什么忙。

所以很多著名的開源項目,為了能用valgrind跑,都費大力氣,大幅修改源代碼,從而使得項目代碼符合valgrind的要求,滿足這些要求的項目,用vargrind跑完沒有任何報警叫valgrind干凈。

既然這些玩意兒都中看不中用,所以,求人不如求己,還得自力更生。

有哪些技術(shù)手段?

可以通過operator new/delete,operator new[]/delete[]重載,但這里有很細致的功夫,你需要全面了解,而不是貿(mào)然行動,建議看看Effective C++,對operator new系列操作符重載有專門的闡述。

你也可以hook malloc、free等系統(tǒng)調(diào)用。

你還可以開啟ptmalloc的調(diào)試功能,它有時候也能管點用。

什么是動態(tài)內(nèi)存分配器?

動態(tài)內(nèi)存分配器是介于kernel跟應(yīng)用程序之間的一個函數(shù)庫,glibc提供的動態(tài)內(nèi)存分配器叫ptmalloc,它也是應(yīng)用最廣泛的動態(tài)內(nèi)存分配器實現(xiàn)。

從kernel角度看,動態(tài)內(nèi)存分配器屬于應(yīng)用程序?qū)?;而從?yīng)用程序的角度看,動態(tài)內(nèi)存分配器屬于系統(tǒng)層。

應(yīng)用程序可以通過mmap系統(tǒng)直接向系統(tǒng)申請動態(tài)內(nèi)存,也可以通過動態(tài)內(nèi)存分配器的malloc接口分配內(nèi)存,而動態(tài)內(nèi)存分配器會通過sbrk、mmap向系統(tǒng)分配內(nèi)存,所以應(yīng)用程序通過free釋放的內(nèi)存,并不一定會真正返還給系統(tǒng),它也有可能被動態(tài)內(nèi)存分配器緩存起來。

所以當你malloc/free配對得很好,但通過top命令去看進程的內(nèi)存占用,還是很高,你不應(yīng)該感到驚訝。

google有自己的動態(tài)內(nèi)存分配器tcmalloc,另外jemalloc也是著名的動態(tài)內(nèi)存分配器,他們有不同的性能表現(xiàn),也有不同的緩存和分配策略。必要的時候,你可以用它們替換linux系統(tǒng)glibc自帶的ptmalloc。

new/delete跟malloc/free的關(guān)系

new是c++的用法,比如Foo *f = new Foo,其實它分為3步。

  1. 通過operator new()分配sizeof(Foo)的內(nèi)存,最終通過malloc分配。
  2. 在新分配的內(nèi)存上構(gòu)建Foo對象。
  3. 返回新構(gòu)建的對象地址。

new=分配內(nèi)存+構(gòu)造+返回,而delete則是等于析構(gòu)+free。

所以搞定malloc、free就是從根本上搞定動態(tài)內(nèi)存分配。

chunk

每次通過malloc返回的一塊內(nèi)存叫一個chunk,動態(tài)內(nèi)存分配器是這樣定義的,后面我們都這樣稱呼。

wrap malloc

gcc支持wrap,即通過傳遞-Wl,--wrap,malloc的方式,可以改變調(diào)用malloc的行為,把對malloc的調(diào)用鏈接到自定義的__wrap_malloc(size_t)函數(shù),而我們可以在__wrap_malloc(size_t)函數(shù)的實現(xiàn)中通過__real_malloc(size_t)真正分配內(nèi)存,而后我們可以做搞點小動作。

同樣,我們可以wrap free。

malloc跟free是配對的,當然也有其他相關(guān)API,比如calloc、realloc、valloc,這些都是細節(jié),根本上還是malloc和free,比如realloc就是malloc + free的組合。

怎么去定位內(nèi)存泄漏呢?

我們會malloc各種不同size的chunk,也就是每種不同size的chunk會有不同數(shù)量,如果我們能夠跟蹤每種size的chunk數(shù)量,那就可以知道哪種size的chunk在泄漏。很簡單,如果該size的chunk數(shù)量一直在增長,那它很可能泄漏。

程序員的福音,C/C++內(nèi)存泄漏的終極解決方案

光知道某種size的chunk泄漏了還不夠,我們得知道是哪個調(diào)用路徑上導(dǎo)致該size的chunk被分配,從而去檢查是不是正確釋放了。

怎么跟蹤到每種size的chunk數(shù)量?

我們可以維護一個全局 unsigned int malloc_map[1024 * 1024]數(shù)組,該數(shù)組的下標就是chunk的size,malloc_map[size]的值就對應(yīng)到該size的chunk分配量。

這等于維護了一個chunk size到chunk count的映射表,它足夠快,而且可以覆蓋到0 ~ 1M大小的chunk的范圍,它已經(jīng)足夠大了,試想一次分配一兆的塊已經(jīng)很恐怖了,可以覆蓋到大部分場景。

那大于1M的塊怎么辦呢?我們可以通過log的方式記錄下來。

在__wrap_malloc里,++malloc_map[size]

在__wrap_free里,--malloc_map[size]

如此一來,我們便通過malloc_map記錄了各size的chunk的分配量。

如何知道釋放的chunk的size?

不對,free(void *p)只有一個參數(shù),我如何知道釋放的chunk的size呢?怎么辦?

我們通過在__wrap_malloc(size_t)的時候,分配8+size的chunk,也就是額外分配8字節(jié),用起始的8字節(jié)存儲該chunk的size,然后返回的是(char*)chunk + 8,也就是偏移8個字節(jié)地址,返回給調(diào)用malloc的應(yīng)用程序。

程序員的福音,C/C++內(nèi)存泄漏的終極解決方案

這樣在free的時候,傳入?yún)?shù)void* p,我們把p往前移動8個字節(jié),解引用就能得到該chunk的大小,而該大小值就是之前在__wrap_malloc的時候設(shè)置的size。

好了,我們真正做到記錄各size的chunk數(shù)量了,它就存在于malloc_map[1M]的數(shù)組中,假設(shè)64個字節(jié)的chunk一直在被分配而沒有被正確回收,最終會表現(xiàn)在malloc_map[size]數(shù)值一直在增長,我們覺得該size的chunk很有可能泄漏,那怎么定位到是哪里調(diào)用過來的呢?

如何記錄調(diào)用鏈?

我們可以維護一個toplist數(shù)組,該數(shù)組假設(shè)有10個元素,它保存的是chunk數(shù)最大的10種size,這個很容易做到,通過對malloc_map取top 10就行。

然后我們在__wrap_malloc(size_t)里,測試該size是不是toplist之一,如果是的話,那我們通過glibc的backtrace把調(diào)用堆棧dump到log文件里去。

注意:這里不能再分配內(nèi)存,所以你只能使用backtrace,而不能使用backtrace_symbols,這樣你只能得到調(diào)用堆棧的符號地址,而不是符號名。

如何把符號地址轉(zhuǎn)換成符號名,也就是對應(yīng)到代碼行呢?答案是addr2line。

addr2line

addr2line工具可以做到,你可以追查到調(diào)用鏈,進而定位到內(nèi)存泄漏的問題。

至此,你已經(jīng)get到了整個核心思想。

當然,實際項目中,我們做的更多,我們不僅僅記錄了toplist size,還記錄了各size chunk的增量toplist,會記錄大塊的malloc/free,會wrap更多的API。

總結(jié)一下:通過wrap malloc/free + backtrace + addr2line,你就可以定位到內(nèi)存泄漏了。

美好的時間過得太快,又是時候說byebye!

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多