问题发现
今天写了一个脚本,提交代码的时候京哥给我cr,果断帮我指出这个脚本的运行时间限制不要这么写
ini_set('max_execution_time','30');
要这么写
set_limit_time(30);
然后给我讲了一堆原理,什么 set_limit_time() 直接进内存啊,ini_set('max_execution_time',) 需要暂时修改原配置啊...恩,还是高工懂得多,于是我开始对两个函数进行了测试。
测试
测试代码如下:
<?php
//测试ini_set
ini_set('max_execution_time',1);
sleep(10);
echo 'begin';
while(true){
}
<?php
//测试set_time_limit
set_time_limit(1);
sleep(10);
echo 'begin';
while(true){
}
这不测不要紧,一测就发现了问题,两次测试都是先sleep了10s,然后返回
beginFatal error: Maximum execution time of 1 second exceeded in /Users/jdq/test.php on line 6
难道sleep不算脚本执行的时间?答案应该是肯定的,可是我以前测试后端接口超时的时候确实用的sleep,而且也超时返回了504,思考了一下应该是php-fpm的配置覆盖了php的ini配置的原因吧,所以sleep的时间也视为一个cgi进程的执行时间。(此处推断有待确定)回归正题,马上修改了测试代码
<?php
//测试ini_set
ini_set('max_execution_time',1);
echo 'begin';
while(true){
}
<?php
//测试set_time_limit
set_time_limit(1);
echo 'begin';
while(true){
}
两个脚本都是执行了1s,直接fatal。那这两个函数又是在什么阶段起作用的呢,修改测试代码为
<?php
echo time(),PHP_EOL;
register_shutdown_function('func');
function func(){
echo time(),PHP_EOL;
}
sleep(5);
ini_set('max_execution_time',5);
echo 'begin',PHP_EOL;
while(true){
}
<?php
echo time(),PHP_EOL;
register_shutdown_function('func');
function func(){
echo time(),PHP_EOL;
}
sleep(5);
set_time_limit(5);
echo 'begin',PHP_EOL;
while(true){
}
返回结果分别为
ini_set结果:
1528297536
begin
Fatal error: Maximum execution time of 5 seconds exceeded in /Users/jdq/test.php on line 11
1528297546
set_time_limit结果:
1528297751
begin
Fatal error: Maximum execution time of 5 seconds exceeded in /Users/jdq/test.php on line 11
1528297761
这两个函数都是在执行的时候才开始限定脚本执行时间,感觉并没有什么区别,所以我找了找两个函数的源码。
函数源码
set_limit_time()
源码如下(以下均为php7.1源码)
PHP_FUNCTION(set_time_limit)
{
zend_long new_timeout;
char *new_timeout_str;
int new_timeout_strlen;
zend_string *key;
//做了一些参数校验
if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &new_timeout) == FAILURE) {
return;
}
new_timeout_strlen = (int)zend_spprintf(&new_timeout_str, 0, ZEND_LONG_FMT, new_timeout);
//看到配置项max_execution_time这里我心里就开始哈哈哈了
key = zend_string_init("max_execution_time", sizeof("max_execution_time")-1, 0);
//其实调用了zend_alter_ini_entry_chars_ex这个函数
if (zend_alter_ini_entry_chars_ex(key, new_timeout_str, new_timeout_strlen, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == SUCCESS) {
RETVAL_TRUE;
} else {
RETVAL_FALSE;
}
zend_string_release(key);
efree(new_timeout_str);
}
通过源码我们可以看出,set_limit_time 函数就是调用了 zend_alter_ini_entry_chars_ex 对配置项 max_execution_time 进行了一番操作,这个函数的源代码
ZEND_API int zend_alter_ini_entry_chars_ex(zend_string *name, const char *value, size_t value_length, int modify_type, int stage, int force_change) /* {{{ */
{
int ret;
zend_string *new_value;
new_value = zend_string_init(value, value_length, !(stage & ZEND_INI_STAGE_IN_REQUEST));
//执行了zend_alter_ini_entry_ex这个函数
ret = zend_alter_ini_entry_ex(name, new_value, modify_type, stage, force_change);
zend_string_release(new_value);
return ret;
}
所以可以看出,set_limit_time 最终实现要是 zend_alter_ini_entry_ex ,下面我们将讨论这个函数。
ini_set
源码如下
PHP_FUNCTION(ini_set)
{
zend_string *varname;
zend_string *new_value;
zend_string *val;
//参数处理
ZEND_PARSE_PARAMETERS_START(2, 2)
Z_PARAM_STR(varname)
Z_PARAM_STR(new_value)
ZEND_PARSE_PARAMETERS_END();
//去一张hash表根据配置项名字寻找当前value,下面会说到,这个value通常会被释放掉
val = zend_ini_get_value(varname);
/* copy to return here, because alter might free it! */
if (val) {
if (ZSTR_IS_INTERNED(val)) {
RETVAL_INTERNED_STR(val);
} else if (ZSTR_LEN(val) == 0) {
RETVAL_EMPTY_STRING();
} else if (ZSTR_LEN(val) == 1) {
RETVAL_INTERNED_STR(ZSTR_CHAR((zend_uchar)ZSTR_VAL(val)[0]));
} else if (!(GC_FLAGS(val) & GC_PERSISTENT)) {
ZVAL_NEW_STR(return_value, zend_string_copy(val));
} else {
ZVAL_NEW_STR(return_value, zend_string_init(ZSTR_VAL(val), ZSTR_LEN(val), 0));
}
} else {
RETVAL_FALSE;
}
//一堆我也不知道要干什么的校验
#define _CHECK_PATH(var, var_len, ini) php_ini_check_path(var, var_len, ini, sizeof(ini))
/* open basedir check */
if (PG(open_basedir)) {
if (_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "error_log") ||
_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.class.path") ||
_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.home") ||
_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "mail.log") ||
_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.library.path") ||
_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "vpopmail.directory")) {
if (php_check_open_basedir(ZSTR_VAL(new_value))) {
zval_dtor(return_value);
RETURN_FALSE;
}
}
}
#undef _CHECK_PATH
//最终要执行zend_alter_ini_entry_ex这个函数
if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) {
zval_dtor(return_value);
RETURN_FALSE;
}
}
ini_set 的实现也是要靠函数 zend_alter_ini_entry_ex ,而 set_limit_time 只是其中一个配置项(参数)为 max_execution_time 的实现而已,原来这两个函数实现机制是一样的,这时京哥的脸色已经发生了微微的变化,哈哈哈......
那么zend_alter_ini_entry_ex又是如何实现的呢?
zend_alter_ini_entry_ex
源码如下
ZEND_API int zend_alter_ini_entry_ex(zend_string *name, zend_string *new_value, int modify_type, int stage, int force_change) /* {{{ */
{
zend_ini_entry *ini_entry;
zend_string *duplicate;
zend_bool modifiable;
zend_bool modified;
//EG(modified_ini_directives)用于存放被修改过的ini_entry,根据name(配置名称)寻找到对应ini_entry
if ((ini_entry = zend_hash_find_ptr(EG(ini_directives), name)) == NULL) {
return FAILURE;
}
modifiable = ini_entry->modifiable;
modified = ini_entry->modified;
if (stage == ZEND_INI_STAGE_ACTIVATE && modify_type == ZEND_INI_SYSTEM) {
ini_entry->modifiable = ZEND_INI_SYSTEM;
}
if (!force_change) {
if (!(ini_entry->modifiable & modify_type)) {
return FAILURE;
}
}
if (!EG(modified_ini_directives)) {
ALLOC_HASHTABLE(EG(modified_ini_directives));
zend_hash_init(EG(modified_ini_directives), 8, NULL, NULL, 0);
}
//不管我们先后在php代码中调用几次ini_set,只有第一次ini_set时才会进入这段逻辑,设置orig_value。从第二次调用ini_set开始,便不会再次执行这段分支,因为此时的modified已经被置为1了。因此,ini_entry->orig_value始终保存的是第一次修改之前的配置值(即最原始的配置)
if (!modified) {
ini_entry->orig_value = ini_entry->value;
ini_entry->orig_modifiable = modifiable;
ini_entry->modified = 1;
zend_hash_add_ptr(EG(modified_ini_directives), ini_entry->name, ini_entry);
}
duplicate = zend_string_copy(new_value);
//调用on_modify是为了能够更新模块的全局变量。每一个ini_entry中都存储了该模块全局变量的地址以及对应的偏移量,使得on_modify可以很迅速的进行内存修改。
if (!ini_entry->on_modify
|| ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS) {
if (modified && ini_entry->orig_value != ini_entry->value) { /* we already changed the value, free the changed value */
zend_string_release(ini_entry->value);
}
ini_entry->value = duplicate;
} else {
zend_string_release(duplicate);
return FAILURE;
}
return SUCCESS;
}
可以看出该函数是同过通过 on_modify 回调函数直接修改了内存中的全局变量而达到控制执行时间的目的,所以这也解释了为什么ini_set 在执行结束就会失效。先说这些,过几天我会整理一下把整个PHP生命周期的ini加载过程详细总结一下。