程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 利用進程信息追查內存洩漏

利用進程信息追查內存洩漏

編輯:關於PHP編程

利用進程信息追查內存洩漏


摘要:內存洩漏是後台服務器程序經常遇見的軟件問題,定位內存洩漏的方法有很多,例如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 0setlogging 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. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved