- 本文地址: https://www.laruence.com/2011/01/27/1854.html
- 转载请注明出处
一个同事forward过来一个, 公司某产品线遇到的一个低概率, 但长时间出现了几次的Core的bt信息, 找我帮忙分析下原因.
bt栈如下(路径信息以*代替):
#0 0x00000000004a75e5 in _zend_mm_alloc_int (heap=0xd61260, size=79) at /*/php-5.2.6/Zend/zend_alloc.c:1879 #1 0x000000000048d3cd in vspprintf (pbuf=0x7fbffe9cd8, max_len=1024, format=Variable "format" is not available. ) at /*/php-5.2.6/main/spprintf.c:224 #2 0x0000000000489747 in php_error_cb (type=1, error_filename=0x2a9a787ee8 "/*/application/helpers/util.php", error_lineno=1149, format=Variable "format" is not available. ) at /*/php-5.2.6/main/main.c:799 #3 0x000000000061db35 in soap_error_handler (error_num=1, error_filename=0x2a9a787ee8 "/*/application/helpers/util.php", error_lineno=1149, format=0x7b9cb8 "Maximum execution time of %d second%s exceeded", args=0x7fbffea3b0) at /*/php-5.2.6/ext/soap/soap.c:2178 #4 0x00000000004c2576 in zend_error (type=1, format=0x7b9cb8 "Maximum execution time of %d second%s exceeded") at /*/php-5.2.6/Zend/zend.c:976 #5 <signal handler called> #6 0x00000000004a720f in _zend_mm_free_int (heap=0xd61260, p=Variable "p" is not available. ) at /*/php-5.2.6/Zend/zend_alloc.c:844 ...以下省略
观察这个调用栈, 会发现#5 ginal handler called, 怎么我记得在zend_mm_free_int的时候, 是屏蔽了interruption的啊? 怎么还会转到信号处理过程呢? 后来证明, 之前看的时候没有深究, 原来在zend_mm_free_int中的:
HANDLE_BLOCK_INTERRUPTIONS(); .... HANDLE_UNBLOCK_INTERRUPTIONS();
就目前来说, 只是个摆设(php-5.2.17之前).
好吧, 不管他了, 那么这个core到底是怎么产生的呢?
首先要介绍下PHP内存管理中的核心结构zend_mm_heap, 如下图:
对于free_buckets来说, 它存储了所有小块内存的指针, 对应的通过一个free_bitmap来指明在free_buckets中, 那些索引是可用的(有实际内存),
然后, 在zend_mm_free_int的函数调用中, 在回收内存的时候, 如果发现内存相邻的内存是空闲的, 则会进行合并, 具体的逻辑:
HANDLE_BLOCK_INTERRUPTIONS(); heap->size -= size; next_block = ZEND_MM_BLOCK_AT(mm_block, size); if (ZEND_MM_IS_FREE_BLOCK(next_block)) { zend_mm_remove_from_free_list(heap, (zend_mm_free_block *) next_block); size += ZEND_MM_FREE_BLOCK_SIZE(next_block); } //以下省略
而在对zend_mm_remove_from_free_list的调用中, 会把原相邻的内存从free_buckets中拿掉:
//有省略 prev->next_free_block = next; next->prev_free_block = prev; if (EXPECTED(ZEND_MM_SMALL_SIZE(ZEND_MM_FREE_BLOCK_SIZE(mm_block)))) { if (EXPECTED(prev == next)) { size_t index = ZEND_MM_BUCKET_INDEX(ZEND_MM_FREE_BLOCK_SIZE(mm_block)); if (EXPECTED(heap->free_buckets[index*2] == heap->free_buckets[index*2+1])) { //注意这一行 heap->free_bitmap &= ~(ZEND_MM_LONG_CONST(1) << index); } } } else if (UNEXPECTED(mm_block->parent != NULL)) { goto subst_block; } //有省略
如上面的代码所示, 在从free_buckets中拿掉这块内存以后, 如果这块内存是对应的index中的唯一一块block, 则会关闭free_bitmap中的可用指示..
问题就出在这里
当代码执行到这句的时候, 业务逻辑超时信号被触发, 导致这行代码没有被执行, 就转入了信号处理流程.
#4 0x00000000004c2576 in zend_error (type=1, format=0x7b9cb8 "Maximum execution time of %d second%s exceeded") at /*/php-5.2.6/Zend/zend.c:976 #5 <signal handler called> #6 0x00000000004a720f in _zend_mm_free_int (heap=0xd61260, p=Variable "p" is not available. ) at /*/php-5.2.6/Zend/zend_alloc.c:844
而在错误处理逻辑中, 又为错误信息申请内存, 而刚好就找到了这个错误的free_bitmap指示的内存:
#0 0x00000000004a75e5 in _zend_mm_alloc_int (heap=0xd61260, size=79) at /*/php-5.2.6/Zend/zend_alloc.c:1879 #1 0x000000000048d3cd in vspprintf (pbuf=0x7fbffe9cd8, max_len=1024, format=Variable "format" is not available. )
而此时, free_buckets中对应的index的指针是一个不可用的指针(指向自身).
从而导致在alloc_init中的逻辑, 段错误退出:
# define ZEND_MM_CHECK_BLOCK_LINKAGE(block) \ if (UNEXPECTED((block)->info._size != ZEND_MM_BLOCK_AT(block, ZEND_MM_FREE_BLOCK_SIZE(block))->info._prev) || \ UNEXPECTED(!UNEXPECTED(ZEND_MM_IS_FIRST_BLOCK(block)) && \ UNEXPECTED(ZEND_MM_PREV_BLOCK(block)->info._size != (block)->info._prev))) { \ zend_mm_panic("zend_mm_heap corrupted"); \ }
原因找到了, 那这个问题怎么避免, 或者处理呢?
其实, 在出core的时候, 业务逻辑已经超时出错了, 而这个core是PHP本身的一些特点造就的, 也不能算是PHP的bug(起码它有一个看似屏蔽信号的操作), 只能说, 低概率的事件吧...
[02-Dec-2016 16:39:10] WARNING: [pool www] child 13131 said into stderr: “zend_mm_heap corrupted”
[02-Dec-2016 16:39:10] WARNING: [pool www] child 13131 exited with code 1 after 47220.351944 seconds from start
[02-Dec-2016 16:39:10] NOTICE: [pool www] child 5094 started
[02-Dec-2016 16:39:10] WARNING: [pool www] child 5094 said into stderr: “ERROR: Unable to set php_value ‘soap.wsdl_cache_dir'”
你好,最近我也遇到这个问题了,头疼了一个月了,该怎么处理呢
现在也碰到了一个类似的问题,是在开发php的扩展中遇到的。扩展本身去调了一个已经编译生成好的库函数,然后就出现了这个zend_mm_heap corrupted. 这种情况没有解决方案么?
[…] 介绍过因为超时信号导致PHP crash的案例:深入理解PHP内存管理之一个低概率Core的分析和一个低概率的PHP Core dump , 在其中, 我说过, 其实PHP在关键操作的时候, […]
其实,这个级别的分析,找出原因也解决不了问题。
顶多满足一下虚荣心。
@Mike Zhang 这个和URL的编码有关系
能不能讲一下PHP是怎么读URL上的参数的。对于http://www….com/test.php?test=测试
这种URL,我4个服务器有3个$_GET[“test”]返回正常,其中一个返回乱码。乱码和正常的服务器中的一个PHP版本一样(5.2.17),正常的3个IIS版本是6.0,7.5,正常的OS有英文版也有中文版。
[…] 本文地址: http://www.laruence.com/2011/01/27/1854.html […]
过来学习中!
[…] 本文地址: http://www.laruence.com/2011/01/27/1854.html […]
@jay 因为屏蔽信号是和系统相关的, 所以PHP把这部分交给了sapi去实现, 而apache2handler, 甚至cli, 都没有对block_interrupt做实现.
鸟哥v5,这么隐蔽的bug都逃不过鸟哥敏锐都眼睛。
不过那个屏蔽中断的宏为什么没有起作用?既然写这个代码都人预见到这段代码有此隐患,为让此宏成为摆设。
后来这个bug如何处理?升级PHP?
这样的bug出现的几率确实太小了,毕竟还是代码上没有做足够的容错处理,也算是一个bug吧