Press "Enter" to skip to content

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
.....

脚本很简单, 通过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了.
而, 第一次请求的时候, 因为页面没有被缓存, 所以apc_stack_size(APCG(cache_stack))的条件判断不成立, 也就不会有清除class table的动作发生.
基于此, 为什么在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虽好, 但须慎用.

17 Comments

  1. think
    think December 11, 2012

    原因理解了,有一个问题还没有解释清楚:APC模块关闭时,为何会需要清空EG(classs_table)?
    按照设想不是只要释放hook就行了吗?

  2. 小哥
    小哥 September 21, 2012

    我们这边也碰到这样的问题。
    不过使用的不是APC, 是eaccelerator。
    也是提示class not found
    不过没有使用session
    那要如何解决呢?
    现在只是把eaccelerator关闭掉。 不过毕竟不是根本的解决方法。 鸟哥有没有再深究一下原因啊。

  3. jianwu5
    jianwu5 April 2, 2012

    天杀的,apc竟然自动关闭apache。
    开启apc后,重启apache,第一次运行phpinfo很慢,再查看apache,又关回去了。
    禁用apc后,apache又正常。
    哎,不知是不是版本不对还是什么,apc是5.2.14,而php是5.2.17

  4. 偷蚊子的
    偷蚊子的 January 12, 2012

    新人,来报道~

  5. hfcorriez
    hfcorriez December 6, 2011

    以前也发现过这个问题,苦于自己没法定位放弃APC了。今天终于知道原因了。

  6. iterse's blog
    iterse's blog September 26, 2011

    恩,学习了。前几天遇到了这个问题,谢谢分享!

  7. abagail
    abagail July 28, 2010

    Zenith Watches Company is a Swiss company that has cartier fake watches breitling chronomat evolution straps have the advantage of being made from a replica a lange and sohne watch automatic watches far from what they like or would buy for swiss replica assioma watches available on the new RSW High King Tourbillon buy watches omega olympic collection watches enhanced and the promotional lights create an replica watches retailers to see what products have arrived in rolex milgauss watches arrives with a long lasting brass situation Other tag heuer fake heard about Gucci They are one of the largest fake watch that updated to make it more suitable to the replica watch out with various styles celebrating this trip to watches replica baguette cut diamonds and baguette cut rubies breitling fake watches libre watches a team of visionary designers and specialists of replica omega under their French cuff shirts and hence they can

  8. 雪候鸟
    雪候鸟 March 20, 2010

    @luckgo 文件不存在那么..

  9. luckgo
    luckgo March 19, 2010

    老兄,我遇到类似问题,但无法解决。
    我在windows下装了php_apc.dll后,访问phpMyAdmin(版本3.1.3)出现错误:
    Warning: require_once(D:\apache\libraries\common.inc.php) [function.require-once]: failed to open stream: No such file or directory in E:\phpweb\phpMyAdmin3\index.php on line 34
    在使用zend framework框架时,出现
    class XXX not found的错误,很奇怪,似乎与session没什么关系,请问有没办法解决?

  10. wakaka
    wakaka January 15, 2010

    博主的PHP和C的功力实在是国内少有的高手。

  11. 雪候鸟
    雪候鸟 December 17, 2009

    @kimjxie 赞~, 这是提前了关闭的时机. 这个bug还是依然值得探讨的. 😉

  12. kimjxie
    kimjxie December 16, 2009

    在代码末 强制执行session_write_close()也可解决问题

  13. phppan
    phppan December 7, 2009

    围观,学习!

  14. 玉丰
    玉丰 December 6, 2009

    最近也在折腾gdb,看了一下这篇文章,(还不会如何去backtrace PHP的运行),呵呵,继续学习。

Comments are closed.