Heim >php教程 >php手册 >利用进程信息追查内存泄漏

利用进程信息追查内存泄漏

WBOY
WBOYOriginal
2016-06-13 08:47:531048Durchsuche

利用进程信息追查内存泄漏

摘要:内存泄漏是后台服务器程序经常遇见的软件问题,定位内存泄漏的方法有很多,例如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>: 0x0 0x0

0x1071010 <_zn7bigpipe13bmq_handler_t16_heart_beat_bodye>: 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> 0x4600000001

0x22c2f10: 0x1 0x117087b

0x22c2f20: 0x0 0x1214495

0x22c2f70: 0x0 0x0

0x22c2f80: 0x0 0x0

0x22c2f90: 0x0 0x0


Gdb.txt中内容的说明和分析:第一列为当前内存地址,如0x22c2f00;第二、三、四列分别为当前内存地址对应所存储的值(使用十六进制表示),以及gdb的debug的符号信息,例如:0x10200d0<_ztvn7bigpipe15bigpipedienginee>0x4600000001,分别表示:“前16字节”、“符号信息(注意有+16的偏移)”、“后16字节”,但不是所有地址都会打印gdb的debug符号信息,有时符号信息显示在第三列,有时显示在第二列。上述这行内存地址0x22c2f00 存储了bigpipe::BigpipeDiEngine 类的生成的其中一个对象的虚析构函数的函数指针,即虚函数表指针(vptr),其中地址0x10200d0附近内存存储的应该是BigpipeDiEngine类的虚函数表(vtbl),如下所示:

SHAPE \* MERGEFORMAT

(gdb) x/a 0x10200d0

0x10200d0 <_ztvn7bigpipe15bigpipedienginee>: 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 "''{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是业界领先的移动应用测试服务平台,为广大开发者在移动应用测试中面临的成本、技术和效率问题提供解决方案。同时分享行业领先的百度技术,作者来自百度员工和业界领袖等。

>>如有问题,欢迎与我沟通

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn