set_limit_time()、ini_set()解析
问题发现
今天写了一个脚本,提交代码的时候京哥给我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加载过程详细总结一下。
相关推荐
<?php. if (!empty($_POST)) {. $data1 = $_POST["data1"];$data2 = $_POST["data2"];$fuhao = $_POST["fuh