4

问题发现

今天写了一个脚本,提交代码的时候京哥给我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加载过程详细总结一下。

参考文章:http://www.cnblogs.com/driftc...


estu
23 声望4 粉丝

我一个人,就是一只军队