PHP ·

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虽好, 但须慎用.

 

参与评论