PHP5.2.x + APC的一个bug的定位
昨天环境迁移, 脚本出core, 因为之前的环境上运行正常, 所以初步认为是环境问题. 通过对core文件的分析, 初步发现原因和spl_autoload相关, backtrace如下:
以下是代码片段: #0 zif_spl_autoload (ht=Variable "ht" is not available.) at /home/huixinchen/package/php-5.2.11/ext/spl/php_spl.c:310 310 if (active_opline->opcode != ZEND_FETCH_CLASS) { (gdb) bt #0 zif_spl_autoload (ht=Variable "ht" is not available. ) at /home/huixinchen/package/php-5.2.11/ext/spl/php_spl.c:310 #1 0x00000000006a5da5 in zend_call_function (fci=0x7fbfffc100, fci_cache=Variable "fci_cache" is not available.) at /home/huixinchen/package/php-5.2.11/Zend/zend_execute_API.c:1052 #2 0x00000000006c558b in zend_call_method (object_pp=0x0, obj_ce=0x0, fn_proxy=0x0, function_name=0x959ab6 "spl_autoload", function_name_len=12, retval_ptr_ptr=0x0, param_count=0, arg1=0x2a95a66cc8, arg2=0x0) at /home/huixinchen/package/php-5.2.11/Zend/zend_interfaces.c:88 ..... |
脚本很简单, 通过session_set_save_handler注册了一个类为session的user handler.
去掉spl_autoload以后, 不出core了, 但是每次都会抛出Class not found的异常, 可见core确实和spl_autoload有关, 但是这个Class ** not found的fatal error问题又和什么相关呢, 这个fatal error是否是导致spl_autoload core 的直接原因呢?
代码本身并没有任何问题, 对环境做了对比以后, 初步认定为新环境启用了APC的缘故.
在bug.php中找到了有人报告类似的bug(spl_autoload crashes when called in write function of custom sessionSaveHandler), 但没有任何一个人给出原因,或者解决的办法.
看来, 只能自己分析了….
精简的代码如下:
以下是代码片段: <?php /** * PHP5.2.11 with APC Fatal Error example * by laruence(http://www.laruence.com) */ class Laruence { public static function start() { session_set_save_handler(array(__CLASS__, "open"), array(__CLASS__, "close"), array(__CLASS__, "read"), array(__CLASS__, "write"), array(__CLASS__, "destroy"), array(__CLASS__, "gc")); session_start(); } public static function open($strPath, $strSessName) { return true; } public static function close() { var_dump(class_exists(__CLASS__, false)); } public static function read($strSessId) { } public static function write($strSessId, $strData) { } public static function destroy($strSessId) { } public static function gc($intMaxLifeTime) { return true; } } Laruence::start(); ?> |
当第一次请求这个页面的时候, 一切正常, 当再次请求的时候, 就会产生:
以下是引用片段: PHP Fatal error: Class ’Laruence’ not found in Unknown on line 0 |
可见, 这个原因一定是和APC缓存了脚本编译结果以后有关.
翻看APC的源码, 发现了一处可怀疑之处, apc_main.c中:
以下是代码片段: static void apc_deactivate(TSRMLS_D) //此函数在请求关闭期间被调用 { while (apc_stack_size(APCG(cache_stack)) > 0) { ...//有省略 if (cache_entry->data.file.classes) { for (i = 0; cache_entry->data.file.classes[i].class_entry != NULL; i++) { if(zend_hash_find(EG(class_table), cache_entry->data.file.classes[i].name, cache_entry->data.file.classes[i].name_len+1, (void**)centry) == FAILURE) { continue; } //注意这里: zend_hash_del(EG(class_table), cache_entry->data.file.classes[i].name, cache_entry->data.file.classes[i].name_len+1); apc_free_class_entry_after_execution(zce); } } ...//有省略 } } |
也就是说, APC在模块请求关闭函数时期, 清空了执行全局标量中的类定义表EG(classs_table), 根据我的经验, 问题可能就在这里.
经过反复验证: 改hanlder, 跟踪源码, gdb,, 最后问题定位确定, 确实就是这个原因(对不住女朋友了, 搞到半夜快2点才最终确定问题):
恩, 我之前的文章介绍过PHP的扩展载入过程(深入理解PHP原理之扩展载入过程), 但没有涉及到模块关闭过程.
而这个问题就和模块载入顺序和模块关闭函数很有关系了. 总体来说, 就是PHP会根据模块载入的顺序的反序来在每次请求处理结束后依次调用各个扩展的请求关闭函数.
因为我们环境的Session是静态编译进PHP的, 所以Session模块一定先于动态编译进PHP的APC被载入, 也就是说, 在请求关闭时期, APC的请求关闭函数, 一定会先于Session的请求关闭函数被调用.
所以,当Session的请求关闭函数调用的时候, 执行环境的Class Table已经为空, 当然也就会抛出类找不到的fatalerror了.
基于此, 为什么在spl_autoload启用以后, 产生core, 也就很明显了.
因为在zif_spl_autoload中, 对active_opline接引用, 而此时执行已经结束, active_opline为空, 所以,segment fault了.
那么, 如何解决这个问题呢?
1. 关闭APC //废话,我也知道, 呵呵 2. 改用函数做为session_set_save_handler的user handler. 3. 把seesion模块做为动态模块载入PHP, 并保证它后于APC被载入. //这个解决方法靠谱.
关于APC的执行原理, 大家如果有兴趣, 我过段再单独写篇blog.
最后, APC是好, 但一致没有被做为PHP的标准扩展, 也是有原因的. 它劫持了PHP自身的complie_file, 加入了很多局部性很强的逻辑…
一句话, APC虽好, 但须慎用.
建议继续学习:
- 使用APC来保护PHP代码 (阅读:4487)
- 关于PHP加速器APC的使用 (阅读:1920)
- 关于APC的性能优化,请看下面这段话 (阅读:1789)
- 小心,apc可能导致php-fpm罢工! (阅读:1758)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:雪候鸟 来源: 风雪之隅
- 标签: APC
- 发布时间:2009-12-06 00:28:27
- [70] Twitter/微博客的学习摘要
- [65] find命令的一点注意事项
- [64] 如何拿下简短的域名
- [64] IOS安全–浅谈关于IOS加固的几种方法
- [63] android 开发入门
- [62] 流程管理与用户研究
- [62] Go Reflect 性能
- [60] Oracle MTS模式下 进程地址与会话信
- [59] 图书馆的世界纪录
- [58] 读书笔记-壹百度:百度十年千倍的29条法则