編輯:關於android開發
摘要:內存洩漏是後台服務器程序經常遇見的軟件問題,定位內存洩漏的方法有很多,例如valgrind,但需要重啟進程。在某些場合下,重啟進程後復現相同的內存洩漏比較困難,或時間較漫長。本文探討一種利用現有已經發生內存洩漏的進程實例進行分析,嘗試獲得內存洩漏點的方法。
一、問題現象
Bigpipe是Baidu公司內部的分布式傳輸系統,其服務器模塊Broker采用異步編程框架來實現,並大量使用了引用計數來管理對象資源的生命周期和釋放時機。在對Broker模塊進行壓力測試過程中,發現Broker長時間運行後,內存占用逐步變大,出現了內存洩漏問題。
二、初步分析
針對近期Broker的升級改造點,確定Broker中可能出現內存洩漏的對象。Broker新增了監控功能,其中一項是對服務器各個參數的監控統計,這必然對參數對象有讀取操作,每次操作都將引用計數“加一”,並在完成操作後“減一”。當前,參數對象有數個,需要確定是哪個參數對象洩漏了。
三、代碼&業務分析
1. 為證明之前的初步分析的結果,可能的方法有是:使用Valgrind運行Broker並啟動壓力程序復現可能的內存洩漏。但是,使用這種方法:
1) 由於內存洩漏的觸發條件並不簡單,可能導致復現周期很長,甚至無法復現同樣的內存洩漏;
2) 內存洩漏的對象放置在容器中,valgrind正常退出後不報告相關的內存洩漏;
經過另外的測試集群短時間的運行嘗試進行復現,果然Valgrind報告未出現異常。
2. 分析現有擁有的條件:幸好,出現“內存洩漏”問題的Broker進程仍然在運行中,真相就在這個進程內部。應該充分利用已有的現場,完成問題的定位。初步希望使用GDB調試。
3. 挑戰:使用GDB attach pid的方法將會導致進程掛起,按Broker的設計,一當配對另一個主/從Broker不互相發送心跳, Broker也將自動退出程序,退出後現場就無法保存,這意味著使用GDB的機會只有一次。
4. 方案:利用gdb打印內存信息並從信息中觀察可能的內存洩漏點。
5. 步驟一:pmap -x {PID}查看內存信息(如:pmap -x 24671);得到類似如下信息,注意標記為anon的位置:
SHAPE \* MERGEFORMAT
24671: ./bin/broker
Address Kbytes RSS Anon Locked Mode Mapping
0000000000400000 11508 - - - r-x-- broker
000000000103c000 388 - - - rw--- broker
000000000109d000 144508 - - - rw--- [ anon ]
00007fb3f583b000 4 - - - rw--- libgcc_s-3.4.5-20051201.so.1
---------------- ------ ------ ------ ------
total kB 610180 - - -
6. 步驟二:啟動gdb ./bin/broker並使用 attach {PID}命令加載現有進程;例如上述進程號為24671,則使用:attach 24671;
7. 步驟三:使用setheight 0 和 setlogging on開啟gdb日志,日志將存儲於gdb.txt文件中;
8. 步驟四:使用x/{內存字節數}a {內存地址} 打印出一段內存信息,例如上述的anon為堆頭地址,占用了144508kb內存,則使用:x/18497024a0x000000000109d000;若命令行較多,可以在外圍編輯好命令行直接張貼至gdb命令行提示符中運行,或者將命令行寫到一個文本文件中,例如command.txt中,然後再gdb命令行提示符中使用 sourcecommand.txt來執行文件中的命令集合,下面是command.txt文件的內容;
SHAPE \* MERGEFORMAT
set height 0
set logging on
x/18497024a 0x000000000109d000
x/23552a 0x000000317ae09000
x/2048a 0x000000317b65e000
x/512a 0x000000318a821000
x/2560a 0x000000318b18d000
9. 步驟五:分析gdb.txt文件中的信息,gdb.txt中的內容如下:
SHAPE \* MERGEFORMAT
0x1071000 <_ZN7bigpipe13bmq_handler_t16_heart_beat_bodyE+832>: 0x0 0x0
0x1071010 <_ZN7bigpipe13bmq_handler_t16_heart_beat_bodyE+848>: 0x0 0x0
…
0x10710c0 <_zgvz5getippce4lock>: 0x0 0x0
0x10710d0 <_zgvzn7bigpipe13bmq_handler_t14get_heart_beaterie4__sl>: 0x0 0x0
0x10710e0 <_zst8__ioinit>: 0x0 0x0
0x10710f0 <_zgvz5getippce4lock>: 0x0 0x0
…
0x22c2f00: 0x10200d0 <_ZTVN7bigpipe14BigpipeDIEngineE+16> 0x4600000001
0x22c2f10: 0x1 0x117087b
0x22c2f20: 0x0 0x1214495
…
0x22c2f70: 0x0 0x0
0x22c2f80: 0x0 0x0
0x22c2f90: 0x0 0x0
…
Gdb.txt中內容的說明和分析:第一列為當前內存地址,如0x22c2f00;第二、三、四列分別為當前內存地址對應所存儲的值(使用十六進制表示),以及gdb的debug的符號信息,例如:0x10200d0<_ZTVN7bigpipe15BigpipeDIEngineE+16>0x4600000001,分別表示:“前16字節”、“符號信息(注意有+16的偏移)”、“後16字節”,但不是所有地址都會打印gdb的debug符號信息,有時符號信息顯示在第三列,有時顯示在第二列。上述這行內存地址0x22c2f00 存儲了bigpipe::BigpipeDiEngine 類的生成的其中一個對象的虛析構函數的函數指針,即虛函數表指針(vptr),其中地址0x10200d0附近內存存儲的應該是BigpipeDiEngine類的虛函數表(vtbl),如下所示:
SHAPE \* MERGEFORMAT
(gdb) x/a 0x10200d0
0x10200d0 <_ZTVN7bigpipe15BigpipeDIEngineE+16>: 0x53e2c6
(gdb) x/i 0x53e2c6
0x53e2c6 : push %rbp
(gdb) x/a 0x53e2c6
0x53e2c6 : 0xec834853e5894855
地址0x10200d0中的值是指向BigpipeDiEngine類的析構函數的地址,即真正的析構函數代碼段頭地址0x53e2c6。可以從上述執行結果看到,地址0x53e2c6的“符號信息”是析構函數名,其匯編命令為push。因此,可以知道最初看到的0x22c2f00地址是對象的一個虛析構函數指針,並且有“符號信息”BigpipeDIEngine顯示出來,可以根據這種信息確定出這個類(帶虛析構函數的類)生成了多少個實例,然後根據排出來的實例個數做進一步判斷。
因此,對gdb.txt排序並做適當處理獲得符號(類名/函數名稱)出現的次數的列表。例如將上述內容過濾出帶尖括號的“符號信息”部分並按出現次數排序,可以使用類似如下命令,catgdb.txt |grep "<"|awk -F '<' '{print $2}' |awk -F '>''{print $1}' |sort |uniq -c|sort -rn > result.txt,過濾出項目相關的變量前綴(如bmq、Bigpipe、bmeta等)cat result.txt|grep -P"bmq|Bigpipe|bigpipe|bmeta"|grep "_ZTV" > result2.txt,獲得類似如下的列表:
SHAPE \* MERGEFORMAT
35782 _ZTVN7bigpipe14CConnectE+16
282 _ZTVN3bsl3var4IVarE+16
179 _ZTVN7bigpipe19bmeta_stripe_info_tE+16
26 _ZTV13AutoKylinLockI5MutexE+16
21 _ZTVN6google8protobuf8internal26GeneratedMessageReflectionE+16
8 _ZTVN6comcfg17ConstraintLibrary12WrapFunctionE+16
8 _ZTVN3bsl3var11BasicStringINS_12basic_stringIcNS_14pool_allocatorIcEEEEEE+16
6 _ZTVN7bigpipe19bmeta_broker_info_tE+16
6 _ZTVN7bigpipe15BigpipeDIEngineE+16
10. 然後找出和本工程項目相關的且出現次數最多的為CConnect對象;判斷出可能洩漏的對象後,還需要定位在異步框架下,哪個引用計數出現了問題導致CConnect對象無法正常減一並得到釋放。
11.經過追查新增的“監控”功能與CConnect相關的代碼,如下。
SHAPE \* MERGEFORMAT
if (atomic_add (&_count, -1) == 0) {
_free(_conn)
}
四、真相大白
查看atomic_add函數的實現(如下),可以得知,返回值是自增(減)之前的值,而由於函數名稱atomic_add並未特別的表現出這樣的含義,導致調用者誤用了這個函數,認為是自增之後的值,最終引用計數誤認為不為0,導致未執行_free操作,進而導致內存洩漏。通常,和__sync_fetch_and_add對應的函數還有__sync_add _and_fetch,這兩者的區別在於“先獲得值再加”還是“先加值在獲取”。
SHAPE \* MERGEFORMAT
atomic_add(volatile int *count, int add)
{
register int __res;
__res = __sync_fetch_and_add(count, add);
return __res;
}
五、解決方案
因此,程序的改進如下:
SHAPE \* MERGEFORMAT
if (atomic_add_and_fetch (&_count, -1) == 0) {
_free(_conn)
}
六、總結
1. 由於異步框架實現的程序對問題定位跟蹤難度較高,需要綜合:日志,gdb,pmap等手段完成問題復現和定位;
2. Valgrind檢測內存洩漏並不是唯一的方法,且具有一定的局限性;
3.函數名稱定義盡量直觀表明函數功能,能夠避免調用方的一部分錯誤;
4.應當仔細閱讀庫函數的說明文檔,了解使用方法;
本方法運用的場景和局限:1)使用gdb打印內存信息中,必須符合實例數和內存信息符號有一對一關系的情形,上述實踐中CConnect類有虛析構函數,因此在內存信息中能查看到虛函數表指針,且和出現的符號有一一對應的關系,由此能作為內存洩漏存在於此類的推測條件;若洩漏的內存在內存信息中沒有留下“痕跡”則無法獲得內存洩漏的有效信息;2)在線下嘗試內存洩漏復現失敗後,但有內存洩漏的進程(現場)在線上仍然存在,可以嘗試使用上述方法,從已有的進程(現場)中更多獲取內存洩漏信息;3)此方法可以利用現有的已經產生內存洩漏的進程(現場)進行分析,充分利用了已有的問題進程;4)上述方法作為其他內存洩漏調試方法的一種補充,一種值得嘗試的方法,可以作為參考。百度MTC是業界領先的移動應用測試服務平台,為廣大開發者在移動應用測試中面臨的成本、技術和效率問題提供解決方案。同時分享行業領先的百度技術,作者來自百度員工和業界領袖等。
>>如有問題,歡迎與我溝通
硅谷新聞1--引導界面GuideActivity,1--guideactivity1.紅點切換間距 RelativeLayout.LayoutParams params
Android系統的五種數據存儲形式(一),android數據存儲 Android系統有五種數據存儲形式,分別
Android 100多個Styles快速開發布局XML,一行搞定View屬性,一鍵統一配置UI...,androidui.. Android開發中大量使用X
手把手搭建自己的android環境,把手搭建android最近想學習安卓,不過國內實在被牆的厲害,真是萬裡安裝只被牆。安裝的過程中也出現了幾個問題。所以記錄下來,免得自己