在做php开发的时候,经常会设置max_input_time、max_execution_time,用来控制脚本的超时时间。但却从来没有思考过背后的原理。
趁着这两天有空,研究一下这个问题。
超时配置
php的ini配置如何起作用,这是一个老生常谈的话题了。
首先,我们在php.ini里进行配置。当php启动的时候(php_module_startup阶段),会尝试读取ini文件并解析。解析过程简单来说,是分析ini文件,提取出其中合法的键值对,并保存到configuration_hash表。
OK,然后php会进一步调用zend_startup_extensions来启动各个模块(包含php Core模块,以及所有需要加载的扩展)。各个模块的启动函数中,会完成REGISTER_INI_ENTRIES动作。REGISTER_INI_ENTRIES负责将模块对应的一些配置从configuration_hash表取出,然后调用处理函数,最终将处理完的值存入模块的globals变量。
max_input_time、max_execution_time这两个配置属于php Core模块。对于php Core来说,REGISTER_INI_ENTRIES依然发生在php_module_startup中。同样属于php Core模块的配置还有expose_php、display_errors、memory_limit等等...
示意图如下:
---->php_module_startup----------->php_request_startup----> | | |-->REGISTER_INI_ENTRIES | | |-->zend_startup_extensions | | | |-->zm_startup_date | | |-->REGISTER_INI_ENTRIES | | | |-->zm_startup_json | | |-->REGISTER_INI_ENTRIES | | |-->do otherthings
上面说到对于不同的配置,REGISTER_INI_ENTRIES会调用不同的函数来处理。我们直接来看max_execution_time对应的函数:
static PHP_INI_MH(OnUpdateTimeout) { // php启动阶段走这里 if (stage == PHP_INI_STAGE_STARTUP) { // 将超时设置保存到EG(timeout_seconds)中 EG(timeout_seconds) = atoi(new_value); return SUCCESS; } // php执行过程中的ini set则走这里 zend_unset_timeout(TSRMLS_C); EG(timeout_seconds) = atoi(new_value); zend_set_timeout(EG(timeout_seconds), 0); return SUCCESS; }
暂时只看上半截,因为我们目前只需关注php的启动阶段,该函数行为很简单,将max_execution_time存入了EG(timeout_seconds)。
至于max_input_time,并没有特殊的处理函数,默认是会将max_input_time存入存入PG(max_input_time)。
因此,当REGISTER_INI_ENTRIES完成,发生的是:
max_execution_time ----> 存入EG(timeout_seconds)
max_input_time ----> 存入PG(max_input_time)
请求超时控制
现在我们搞清楚php的启动阶段发生了什么,继续来看php在实际处理请求的时候,如何管理超时。
在php_request_startup函数中有如下代码:
if (PG(max_input_time) == -1) { zend_set_timeout(EG(timeout_seconds), 1); } else { zend_set_timeout(PG(max_input_time), 1); }
php_request_startup的时机很讲究。
以cgi为例,只有当php已经从CGI拿到了原始请求以及一些CGI的环境变量之后,php_request_startup才会被调用。上面这段代码实际执行的时候,由于请求已经拿到,所以SG(request_info)处于准备就绪状态,但是php中的$_GET,$_POST,$_FILE等超全局变量尚未生成。
从代码上理解:
1、如果用户将max_input_time配做-1,或没有配置,那么脚本的生命周期就只受EG(timeout_seconds)约束。
2、否则,请求启动阶段的超时控制,受PG(max_input_time)约束。
3、zend_set_timeout函数负责设置定时器。一旦指定时间过去,定时器会通知php进程。zend_set_timeout下文会具体分析。
php_request_startup完成,则进入php的实际执行阶段,即php_execute_script。在php_execute_script中可以看到:
// 设定执行超时 if (PG(max_input_time) != -1) { #ifdef PHP_WIN32 zend_unset_timeout(TSRMLS_C); // 关闭之前的定时器 #endif zend_set_timeout(INI_INT("max_execution_time"), 0); } // 进入执行 retval = (zend_execute_scripts(ZEND_REQUIRE TSRMLS_CC, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);
OK,假如代码执行到这里,尚未发生max_input_time超时,则会重新指定max_execution_time的超时。
同样也是采取调用zend_set_timeout,并传入max_execution_time。特别注意一下,windows下面的需要显式调用zend_unset_timeout关闭原来的定时器,而linux下不需要。这是由于两个平台的定时器实现原理不同导致的,下文也会详细展开叙述。
最后用一张图表示超时控制的流程,左侧的case表明用户既配置了max_input_time,又配置了max_execution_time。而右侧的区别在于用户仅仅配置了max_execution_time:
zend_set_timeout
前文提到,zend_set_timeout函数用来设置定时器。具体来看下实现: