阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

深入理解PHP中的ini配置

515次阅读
没有评论

共计 34123 个字符,预计需要花费 86 分钟才能阅读完成。

这篇文章不会详细叙述某个 ini 配置项的用途,这些在手册上已经讲解的面面俱到。我只是想从某个特定的角度去挖掘 php 的实现机制,会涉及到一些 php 内核方面的知识:-)

使用 php 的同学都知道 php.ini 配置的生效会贯穿整个 SAPI 的生命周期。在一段 php 脚本的执行过程中,如果手动修改 ini 配置,是不会启作用的。此时如果无法重启 apache 或者 nginx 等,那么就只能显式的在 php 代码中调用 ini_set 接口。ini_set是 php 向我们提供的一个动态修改配置的函数,需要注意的是,利用 ini_set 所设置的配置与 ini 文件中设置的配置,其生效的时间范围并不相同。在 php 脚本执行结束之后,ini_set 的设置便会随即失效。

因此本文打算分两篇,第一篇阐述 php.ini 配置原理,第二篇讲动态修改 php 配置。

php.ini 的配置大致会涉及到三块数据,configuration_hash,EG(ini_directives)以及 PG、BG、PCRE_G、JSON_G、XXX_G 等。如果不清楚这三种数据的含义也没有关系,下文会详细解释。

1,解析 INI 配置文件

由于 php.ini 需要在 SAPI 过程中一直生效,那么解析 ini 文件并据此来构建 php 配置的工作,必定是发生 SAPI 的一开始。换句话说,也就是必定发生在 php 的启动过程中。php 需要任意一个实际的请求到达之前,其内部已经生成好这些配置。

反映到 php 的内核,即为 php_module_startup 函数。

php_module_startup 主要负责对 php 进行启动,通常它会在 SAPI 开始的时候被调用。btw,还有一个常见的函数是 php_request_startup,它负责将在每个请求到来的时刻进行初始化,php_module_startup 与 php_request_startup 是两个标识性的动作,不过对他们进行分析并不在本文的探讨范围内。

举个例子,当 php 挂接在 apache 下面做一个 module,那么 apache 启动的时候,便会激活所有这些 module,其中包括 php module。在激活 php module 时,便会调用到 php_module_startup。php_module_startup 函数完成了茫茫多的工作,一旦 php_module_startup 调用结束就意味着,OK,php 已经启动,现在可以接受请求并作出响应了。

在 php_module_startup 函数中,与解析 ini 文件相关的实现是:

/* this will read in php.ini, set up the configuration parameters,
   load zend extensions and register php function extensions
   to be loaded later */
if (php_init_config(TSRMLS_C) == FAILURE) {return FAILURE;
}

可以看到,其实就是调用了 php_init_config 函数,去完成对 ini 文件的 parse。parse 工作主要进行 lex&grammar 分析,并将 ini 文件中的 key、value 键值对提取出来并保存。php.ini 的格式很简单,等号左侧为 key,右侧为 value。每当一对 kv 被提取出来之后,php 将它们存储到哪儿呢?答案就是之前提到的 configuration_hash。

static HashTable configuration_hash;

configuration_hash 声明在 php_ini.c 中,它是一个 HashTable 类型的数据结构。顾名思义,其实就是张 hash 表。题外话,在 php5.3 之前的版本是没法获取 configuration_hash 的,因为它是 php_ini.c 文件的一个 static 的变量。后来 php5.3 添加了 php_ini_get_configuration_hash 接口,该接口直接返回 &configuration_hash,使 得 php 各个扩展可以方便的一窥 configuration_hash 全貌 … 真是普大喜奔 …

注意四点:

第一,php_init_config 不会做除了词法语法以外的任何校验。也就是说,假如我们在 ini 文件中添加一行 hello=world,只要这是一个格式正确的配置项,那么最终 configuration_hash 中就会包含一个键为 hello、值为 world 的元素,configuration_hash 最大限度的反映出 ini 文件。

第二,ini 文件允许我们以数组的形式进行配置。例如 ini 文件中写入以下三行:

drift.arr[]=1
drift.arr[]=2
drift.arr[]=3

那么最终生成的 configuration_hash 表中,就会存在一个 key 为 drift.arr 的元素,其 value 为一个包含的 1,2,3 三个数字的数组。这是一种极为罕见的配置方法。

第三,php 还允许我们除了默认的 php.ini 文件(准确说是 php-%s.ini)之外,另外构建一些 ini 文件。这些 ini 文件会被放入一个额外的目录。该目录由环境变量 PHP_INI_SCAN_DIR 来指定,当 php_init_config 解析完了 php.ini 之后,会再次扫描此目录,然后找出目录中所有.ini 文件来分析。这些额外的 ini 文件中产生的 kv 键值对,也会被加入到 configuration_hash 中去。

这是一个偶尔有用的特性,假设我们自己开发 php 的扩展,却又不想将配置混入 php.ini,便可以另外写一份 ini,并通过 PHP_INI_SCAN_DIR 告诉 php 该去哪儿找到它。当然,其缺点也显而易见,其需要设置额外的环境变量来支持。更好的解决办法是,开发者在扩展中自己调用 php_parse_user_ini_file 或 zend_parse_ini_file 去解析对应的 ini 文件。

第四,在 configuration_hash 中,key 是字符串,那么值的类型是什么?答案也是字符串(除了上述很特殊的数组)。具体来说,比如下面的配置:

display_errors = On
log_errors = Off
log_errors_max_len = 1024

 那么最后 configuration_hash 中实际存放的键值对为:

key: "display_errors"
val : "1"

key: "log_errors"
val : ""

key: "log_errors_max_len"
val : "1024"

注意 log_errors,其存放的值连 ”0″ 都不是,就是一个实实在在地空字符串。另外,log_errors_max_len 也并非数字,而是字符串 1024。

分析至此,基本上解析 ini 文件相关的内容都说清楚了。简单总结一下:

1,解析 ini 发生在 php_module_startup 阶段

2,解析结果存放在 configuration_hash 里。

2,配置作用到模块

php 的大致结构可以看成是最下层有一个 zend 引擎,它负责与 OS 进行交互、编译 php 代码、提供内存托管等等,在 zend 引擎的上层,排列着很多很多的模块。其中最核心的就一个 Core 模块,其他还有比如 Standard,PCRE,Date,Session 等等 … 这些模块还有另一个名字叫 php 扩展。我们可以简单理解为,每个模块都会提供一组功能接口给开发者来调用,举例来说,常用的诸如 explode,trim,array 等内置函数,便是由 Standard 模块提供的。

为什么需要谈到这些,是因为在 php.ini 里除了针对 php 自身,也就是针对 Core 模块的一些配置(例如 safe_mode,display_errors,max_execution_time 等),还有相当多的配置是针对其他不同模块的。

例如,date 模块,它提供了常见的 date,time,strtotime 等函数。在 php.ini 中,它的相关配置形如:

[Date]
;date.timezone = 'Asia/Shanghai'
;date.default_latitude = 31.7667
;date.default_longitude = 35.2333
;date.sunrise_zenith = 90.583333
;date.sunset_zenith = 90.583333

除了这些模块拥有独立的配置,zend 引擎也是可配的,只不过 zend 引擎的可配项非常少,只有 error_reporting,zend.enable_gc 和 detect_unicode 三项。

在上一小节中我们已经谈到,php_module_startup 会调用 php_init_config,其目的是解析 ini 文件并生成 configuration_hash。那么接下来在 php_module_startup 中还会做什么事情呢?很显然,就是会将 configuration_hash 中的配置作用于 Zend,Core,Standard,SPL 等不同模块。当然这并非一个一蹴而就的过程,因为 php 通常会包含有很多模块,php 启动的过程中这些模块也会依次进行启动。那么,对模块 A 进行配置的过程,便是发生在模块 A 的启动过程中。

有扩展开发经验的同学会直接指出,模块 A 的启动不就是在 PHP_MINIT_FUNCTION(A)中么?

是的,如果模块 A 需要配置,那么在 PHP_MINIT_FUNCTION 中,可以调用 REGISTER_INI_ENTRIES()来完成。REGISTER_INI_ENTRIES 会根据当前模块所需要的配置项名称,去 configuration_hash 查找用户设置的配置值,并更新到模块自己的全局空间中。

2.1,模块的全局空间

要理解如何将 ini 配置从 configuration_hash 作用到各个模块之前,有必要先了解一下 php 模块的全局空间。对于不同的 php 模块,均可以开辟一块属于自己的存储空间,并且这块空间对于该模块来说,是全局可见的。一般而言,它会被用来存放该模块所需的 ini 配置。也就是说,configuration_hash 中的配置项,最终会被存放到该全局空间中。在模块的执行过程中,只需要直接访问这块全局空间,就可以拿到用户针对该模块进行的设置。当然,它也经常被用来记录模块在执行过程中的中间数据。

我们以 bcmath 模块来举例说明,bcmath 是一个提供数学计算方面接口的 php 模块,首先我们来看看它有哪些 ini 配置:

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("bcmath.scale", "0", PHP_INI_ALL, OnUpdateLongGEZero, bc_precision, zend_bcmath_globals, bcmath_globals)
PHP_INI_END()

bcmath 只有一个配置项,我们可以在 php.ini 中用bcmath.scale 来配置bcmath 模块。

接下来继续看看 bcmatch 模块的全局空间定义。在 php_bcmath.h 中有如下声明:

ZEND_BEGIN_MODULE_GLOBALS(bcmath)
   bc_num _zero_;
   bc_num _one_;
   bc_num _two_;
   long bc_precision;
ZEND_END_MODULE_GLOBALS(bcmath)

 宏展开之后,即为:

typedef struct _zend_bcmath_globals {
    bc_num _zero_;
    bc_num _one_;
    bc_num _two_;
    long bc_precision;
} zend_bcmath_globals;

其实,zend_bcmath_globals 类型 就是 bcmath 模块中的全局空间类型。这里仅仅声明了zend_bcmath_globals 结构体,在bcmath.c 中还有具体的实例化定义:

// 展开后即为 zend_bcmath_globals bcmath_globals;
ZEND_DECLARE_MODULE_GLOBALS(bcmath)

可以看出,用 ZEND_DECLARE_MODULE_GLOBALS 完成了对变量 bcmath_globals 的定义。

bcmath_globals 是一块真正的全局空间,它包含有四个字段。其最后一个字段 bc_precision,对应于 ini 配置中的 bcmath.scale。我们在 php.ini 中设置了bcmath.scale 的值,随后在启动 bcmath 模块的时候,bcmath.scale 的值 被更新到 bcmath_globals.bc_precision 中去。

把 configuration_hash 中的值,更新到各个模块自己定义的 xxx_globals 变量中,就是所谓的将 ini 配置作用到模块。一旦模块启动完成,那么这些配置也都作用到位。所以 在随后的执行阶段,php 模块无需再次访问 configuration_hash,模块仅需要访问自己的 XXX_globals,就可以拿到用户设定的配置。

bcmath_globals,除了有一个字段为 ini 配置项,其他还有三个字段为何意?这就是模块全局空间的第二个作用,它除了用于 ini 配置,还可以存储模块执行过程中的一些数据。

深入理解 PHP 中的 ini 配置

再例如 json 模块,也是 php 中一个很常用的模块:

ZEND_BEGIN_MODULE_GLOBALS(json)
    int error_code;
ZEND_END_MODULE_GLOBALS(json)

可以看到 json 模块并不需要 ini 配置,它的全局空间只有一个字段 error_code。error_code 记录了上一次执行 json_decode 或者 json_encode 中发生的错误。json_last_error 函数便是返回这个 error_code,来帮助用户定位错误原因。

为了能够很便捷的访问模块全局空间变量,php 约定俗成的提出了一些宏。比如我们想访问 json_globals 中的 error_code,当然可以直接写做 json_globals.error_code(多线程环境下不行),不过更通用的写法是定义 JSON_G 宏:

#define JSON_G(v) (json_globals.v)

我们使用 JSON_G(error_code)来访问 json_globals.error_code。本文刚开始的时候,曾提到 PG、BG、JSON_G、PCRE_G,XXX_G 等等,这些宏在 php 源代码中也是很常见的。现在我们可以很轻松的理解它们,PG 宏可以访问 Core 模块的全局变量,BG 访问 Standard 模块的全局变量,PCRE_G 则访问 PCRE 模块的全局变量。

#define PG(v) (core_globals.v)
#define BG(v) (basic_globals.v)

2.2,如何确定一个模块需要哪些配置呢?

模块需要什么样的 INI 配置,都是在各个模块中自己定义的。举例来说,对于 Core 模块,有如下的配置项定义:

PHP_INI_BEGIN()
    ......
    STD_PHP_INI_ENTRY_EX("display_errors", "1", PHP_INI_ALL,    OnUpdateDisplayErrors, display_errors, php_core_globals, core_globals, display_errors_mode)
    STD_PHP_INI_BOOLEAN("enable_dl",       "1", PHP_INI_SYSTEM, OnUpdateBool,          enable_dl,      php_core_globals, core_globals)
    STD_PHP_INI_BOOLEAN("expose_php",      "1", PHP_INI_SYSTEM, OnUpdateBool,          expose_php,     php_core_globals, core_globals)
    STD_PHP_INI_BOOLEAN("safe_mode",       "0", PHP_INI_SYSTEM, OnUpdateBool,          safe_mode,      php_core_globals, core_globals)
    ......
PHP_INI_END()

可以在 php-src\main\main.c 文件大概 450+ 行找到上述代码。其中涉及的宏比较多,有 ZEND_INI_BEGIN、ZEND_INI_END、PHP_INI_ENTRY_EX、STD_PHP_INI_BOOLEAN等等,本文不一一赘述,感兴趣的读者可自行分析。

上述代码进行宏展开后得到:

static const zend_ini_entry ini_entries[] = {
    ..
    {0, PHP_INI_ALL,    "display_errors",sizeof("display_errors"),OnUpdateDisplayErrors,(void *)XtOffsetOf(php_core_globals, display_errors), (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, display_errors_mode },
    {0, PHP_INI_SYSTEM, "enable_dl",     sizeof("enable_dl"),     OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, enable_dl),      (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    {0, PHP_INI_SYSTEM, "expose_php",    sizeof("expose_php"),    OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, expose_php),     (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    {0, PHP_INI_SYSTEM, "safe_mode",     sizeof("safe_mode"),     OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, safe_mode),      (void *)&core_globals, NULL, "0", sizeof("0")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    ...
    {0, 0, NULL, 0, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 0, 0, NULL }
};

我们看到,配置项的定义,其本质上就是定义了一个 zend_ini_entry 类型的数组。zend_ini_entry 结构体的字段具体含义为:

struct _zend_ini_entry {int module_number;                // 模块的 id
    int modifiable;                   // 可被修改的范围,例如 php.ini,ini_set
    char *name;                       // 配置项的名称
    uint name_length;
    ZEND_INI_MH((*on_modify));        // 回调函数,配置项注册或修改的时候会调用
    void *mh_arg1;                    // 通常为配置项字段在 XXX_G 中的偏移量
    void *mh_arg2;                    // 通常为 XXX_G
    void *mh_arg3;                    // 通常为保留字段,极少用到

    char *value;                      // 配置项的值
    uint value_length;

    char *orig_value;                 // 配置项的原始值
    uint orig_value_length;
    int orig_modifiable;              // 配置项的原始 modifiable
    int modified;                     // 是否发生过修改,如果有修改,则 orig_value 会保存修改前的值

    void (*displayer)(zend_ini_entry *ini_entry, int type);
};

2.3,将配置作用到模块——REGISTER_INI_ENTRIES

经常能够在不同扩展的 PHP_MINIT_FUNCTION 里看到 REGISTER_INI_ENTRIES。REGISTER_INI_ENTRIES 主要负责完成两件事情,第一,对模块的全局空间 XXX_G 进行填充,同步 configuration_hash 中的值 到 XXX_G 中去。其次,它还生成了 EG(ini_directives)。

REGISTER_INI_ENTRIES 也是一个宏,展开之后实则为 zend_register_ini_entries 方法。具体来看下 zend_register_ini_entries 的实现:

ZEND_API int zend_register_ini_entries(const zend_ini_entry *ini_entry, int module_number TSRMLS_DC) /* {{{*/
{// ini_entry 为 zend_ini_entry 类型数组,p 为数组中每一项的指针
    const zend_ini_entry *p = ini_entry;
    zend_ini_entry *hashed_ini_entry;
    zval default_value;
    
    // EG(ini_directives)就是 registered_zend_ini_directives
    HashTable *directives = registered_zend_ini_directives;
    zend_bool config_directive_success = 0;
    
    // 还记得 ini_entry 最后一项固定为 {0, 0, NULL, ...} 么
    while (p->name) {config_directive_success = 0;
        
        // 将 p 指向的 zend_ini_entry 加入 EG(ini_directives)
        if (zend_hash_add(directives, p->name, p->name_length, (void*)p, sizeof(zend_ini_entry), (void **) &hashed_ini_entry) == FAILURE) {zend_unregister_ini_entries(module_number TSRMLS_CC);
            return FAILURE;
        }
        hashed_ini_entry->module_number = module_number;
        
        // 根据 name 去 configuration_hash 中查询,取出来的结果放在 default_value 中
        // 注意 default_value 的值比较原始,一般是数字、字符串、数组等,具体取决于 php.ini 中的写法
        if ((zend_get_configuration_directive(p->name, p->name_length, &default_value)) == SUCCESS) {// 调用 on_modify 更新到模块的全局空间 XXX_G 中
            if (!hashed_ini_entry->on_modify || hashed_ini_entry->on_modify(hashed_ini_entry, Z_STRVAL(default_value), Z_STRLEN(default_value), hashed_ini_entry->mh_arg1, hashed_ini_entry->mh_arg2, hashed_ini_entry->mh_arg3, ZEND_INI_STAGE_STARTUP TSRMLS_CC) == SUCCESS) {hashed_ini_entry->value = Z_STRVAL(default_value);
                hashed_ini_entry->value_length = Z_STRLEN(default_value);
                config_directive_success = 1;
            }
        }

        // 如果 configuration_hash 中没有找到,则采用默认值
        if (!config_directive_success && hashed_ini_entry->on_modify) {hashed_ini_entry->on_modify(hashed_ini_entry, hashed_ini_entry->value, hashed_ini_entry->value_length, hashed_ini_entry->mh_arg1, hashed_ini_entry->mh_arg2, hashed_ini_entry->mh_arg3, ZEND_INI_STAGE_STARTUP TSRMLS_CC);
        }
        p++;
    }
    return SUCCESS;
}

简单来说,可以把上述代码的逻辑表述为:

1,将模块声明的 ini 配置项添加到 EG(ini_directives)中。注意,ini 配置项的值可能在随后被修改。

2,尝试去 configuration_hash 中寻找各个模块需要的 ini。

  • 如果能够找到,说明用户叜 ini 文件中配置了该值,那么采用用户的配置。
  • 如果没有找到,OK,没有关系,因为模块在声明 ini 的时候,会带上默认值。

3,将 ini 的值同步到 XX_G 里面。毕竟在 php 的执行过程中,起作用的还是这些 XXX_globals。具体的过程是调用每条 ini 配置对应的 on_modify 方法完成,on_modify 由模块在声明 ini 的时候进行指定。

我们来具体看下 on_modify,它其实是一个函数指针,来看两个具体的 Core 模块的配置声明:

STD_PHP_INI_BOOLEAN("log_errors",      "0",    PHP_INI_ALL, OnUpdateBool, log_errors,         php_core_globals, core_globals)
STD_PHP_INI_ENTRY("log_errors_max_len","1024", PHP_INI_ALL, OnUpdateLong, log_errors_max_len, php_core_globals, core_globals)

对于 log_errors,它的 on_modify 被设置为 OnUpdateBool,对于 log_errors_max_len,则 on_modify 被设置为 OnUpdateLong。

进一步假设我们在 php.ini 中的配置为:

log_errors = On
log_errors_max_len = 1024

具体来看下 OnUpdateBool 函数:

ZEND_API ZEND_INI_MH(OnUpdateBool) 
{zend_bool *p;
    
    // base 表示 core_globals 的地址
    char *base = (char *) mh_arg2;

    // p 表示 core_globals 的地址加上 log_errors 字段的偏移量
    // 得到的即为 log_errors 字段的地址
    p = (zend_bool *) (base+(size_t) mh_arg1);  

    if (new_value_length == 2 && strcasecmp("on", new_value) == 0) {*p = (zend_bool) 1;
    }
    else if (new_value_length == 3 && strcasecmp("yes", new_value) == 0) {*p = (zend_bool) 1;
    }
    else if (new_value_length == 4 && strcasecmp("true", new_value) == 0) {*p = (zend_bool) 1;
    }
    else {// configuration_hash 中存放的 value 是字符串 "1",而非 "On"
        // 因此这里用 atoi 转化成数字 1 
        *p = (zend_bool) atoi(new_value);
    }
    return SUCCESS;
}

最令人费解的估计就是 mh_arg1 和 mh_arg2 了,其实对照前面所述的 zend_ini_entry 定义,mh_arg1,mh_arg2 还是很容易参透的。mh_arg1 表示字节偏移量,mh_arg2表示 XXX_globals 的地址。因此,(char *)mh_arg2 + mh_arg1 的结果即为 XXX_globals 中某个字段的地址。具体到本 case 中,就是计算 core_globals中 log_errors 的地址。因此,当 OnUpdateBool 最后执行到

*p = (zend_bool) atoi(new_value);

其作用就相当于

core_globals.log_errors(zend_bool) atoi(“1”);

分析完了OnUpdateBool,我们再来看OnUpdateLong 便觉得一目了然:

ZEND_API ZEND_INI_MH(OnUpdateLong)
{long *p;
    char *base = (char *) mh_arg2;

    // 获得 log_errors_max_len 的地址
    p = (long *) (base+(size_t) mh_arg1);

    // 将 "1024" 转化成 long 型,并赋值给 core_globals.log_errors_max_len
    *p = zend_atol(new_value, new_value_length);
    return SUCCESS;
}

最后需要注意的是,zend_register_ini_entries 函数中,如果 configuration_hash 中存在配置,则当调用 on_modify 结束后,hashed_ini_entry 中的 value 和 value_length 会被更新。也就是说,如果用户在 php.ini 中配置过,则 EG(ini_directives)存放的就是实际配置的值。如果用户没配,EG(ini_directives)中存放的是声明 zend_ini_entry 时给出的默认值。

zend_register_ini_entries 中的 default_value 变量命名比较糟糕,相当容易造成误解。其实 default_value 并非表示默认值,而是表示用户实际配置的值。

3,总结

至此,三块数据 configuration_hash,EG(ini_directives)以及 PG、BG、PCRE_G、JSON_G、XXX_G… 已经都交代清楚了。

总结一下:

1,configuration_hash,存放 php.ini 文件里的配置,不做校验,其值为字符串。
2,EG(ini_directives),存放的是各个模块中定义的 zend_ini_entry,如果用户在 php.ini 配置过(configuration_hash 中存在),则值被替换为 configuration_hash 中的值,类型依然是字符串。
3,XXX_G,该宏用于访问模块的全局空间,这块内存空间可用来存放 ini 配置,并通过 on_modify 指定的函数进行更新,其数据类型由 XXX_G 中的字段声明来决定。

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2016-02/128442p2.htm

1,运行时改变 配置

在前一篇中曾经谈到,ini_set 函数可以在 php 执行的过程中,动态修改 php 的部分配置。注意,仅仅是部分,并非所有的配置都可以动态修改。关于 ini 配置的可修改性,参见:http://php.net/manual/zh/configuration.changes.modes.php

我们直接进入 ini_set 的实现,函数虽然有点长,但是逻辑很清晰:

PHP_FUNCTION(ini_set)
{char *varname, *new_value;
    int varname_len, new_value_len;
    char *old_value;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &varname, &varname_len, &new_value, &new_value_len) == FAILURE) {return;
    }

    // 去 EG(ini_directives)中获取配置的值
    old_value = zend_ini_string(varname, varname_len + 1, 0);

    /* copy to return here, because alter might free it! */
    if (old_value) {RETVAL_STRING(old_value, 1);
    } else {RETVAL_FALSE;}

    // 如果开启了安全模式,那么如下这些 ini 配置可能涉及文件操作,需要要辅助检查 uid
#define _CHECK_PATH(var, var_len, ini) php_ini_check_path(var, var_len, ini, sizeof(ini))
    /* safe_mode & basedir check */
    if (PG(safe_mode) || PG(open_basedir)) {if (_CHECK_PATH(varname, varname_len, "error_log") ||
            _CHECK_PATH(varname, varname_len, "Java.class.path") ||
            _CHECK_PATH(varname, varname_len, "java.home") ||
            _CHECK_PATH(varname, varname_len, "mail.log") ||
            _CHECK_PATH(varname, varname_len, "java.library.path") ||
            _CHECK_PATH(varname, varname_len, "vpopmail.directory")) {if (PG(safe_mode) && (!php_checkuid(new_value, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {zval_dtor(return_value);
                RETURN_FALSE;
            }
            if (php_check_open_basedir(new_value TSRMLS_CC)) {zval_dtor(return_value);
                RETURN_FALSE;
            }
        }
    }

    // 在安全模式下,如下这些 ini 受到保护,不会被动态修改
    if (PG(safe_mode)) {if (!strncmp("max_execution_time", varname, sizeof("max_execution_time")) ||
            !strncmp("memory_limit", varname, sizeof("memory_limit")) ||
            !strncmp("child_terminate", varname, sizeof("child_terminate"))
        ) {zval_dtor(return_value);
            RETURN_FALSE;
        }
    }

    // 调用 zend_alter_ini_entry_ex 去动态修改 ini 配置
    if (zend_alter_ini_entry_ex(varname, varname_len + 1, new_value, new_value_len, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0 TSRMLS_CC) == FAILURE) {zval_dtor(return_value);
        RETURN_FALSE;
    }
}

可以看到,除了一些必要的验证工作,主要就是调用 zend_alter_ini_entry_ex。

我们继续跟进到 zend_alter_ini_entry_ex 函数中:

ZEND_API int zend_alter_ini_entry_ex(char *name, uint name_length, char *new_value, uint new_value_length, int modify_type, int stage, int force_change TSRMLS_DC) /* {{{*/
{zend_ini_entry *ini_entry;
    char *duplicate;
    zend_bool modifiable;
    zend_bool modified;

    // 找出 EG(ini_directives)中对应的 ini_entry
    if (zend_hash_find(EG(ini_directives), name, name_length, (void **) &ini_entry) == FAILURE) {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;
        }
    }

    // EG(modified_ini_directives)用于存放被修改过的 ini_entry
// 主要用做恢复 if (!EG(modified_ini_directives)) {ALLOC_HASHTABLE(EG(modified_ini_directives)); zend_hash_init(EG(modified_ini_directives), 8, NULL, NULL, 0); } // 将 ini_entry 中的值,值的长度,可修改范围,保留到 orig_xxx 中去
// 以便在请求结束的时候,可以对 ini_entry 做恢复
if (!modified) {ini_entry->orig_value = ini_entry->value; ini_entry->orig_value_length = ini_entry->value_length; ini_entry->orig_modifiable = modifiable; ini_entry->modified = 1; zend_hash_add(EG(modified_ini_directives), name, name_length, &ini_entry, sizeof(zend_ini_entry*), NULL); } duplicate = estrndup(new_value, new_value_length); // 调用 modify 来更新 XXX_G 中对应的 ini 配置 if (!ini_entry->on_modify || ini_entry->on_modify(ini_entry, duplicate, new_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage TSRMLS_CC) == SUCCESS) {// 同上面,如果多次修改,则需要释放前一次修改的值 if (modified && ini_entry->orig_value != ini_entry->value) { efree(ini_entry->value); } ini_entry->value = duplicate; ini_entry->value_length = new_value_length; } else {efree(duplicate); return FAILURE; } return SUCCESS; }

有 3 处逻辑需要我们仔细体会:

1)ini_entry 中的 modified 字段用来表示该配置是否被动态修改过。一旦该 ini 配置发生修改,modified 就会被置为 1。上述代码中有一段很关键:

// 如果多次调用 ini_set,则 orig_value 等始终保持最原始的值
if (!modified) {ini_entry->orig_value = ini_entry->value;
    ini_entry->orig_value_length = ini_entry->value_length;
    ini_entry->orig_modifiable = modifiable;
    ini_entry->modified = 1;
    zend_hash_add(EG(modified_ini_directives), name, name_length, &ini_entry, sizeof(zend_ini_entry*), NULL);
}

这段代码表示,不管我们先后在 php 代码中调用几次 ini_set,只有第一次 ini_set 时才会进入这段逻辑,设置好 orig_value。从第二次调用 ini_set 开始,便不会再次执行这段分支,因为此时的 modified 已经被置为 1 了。因此,ini_entry->orig_value 始终保存的是第一次修改之前的配置值(即最原始的配置)。

2)为了能使 ini_set 修改的配置立即生效,需要 on_modify 回调函数。

如前一篇文中所述,调用 on_modify 是为了能够更新模块的全局变量。再次回忆下,首先,模块全局变量中的配置已经不是字符串类型了,该用 bool 用 bool、该用 int 用 int。其次,每一个 ini_entry 中都存储了该模块全局变量的地址以及对应的偏移量,使得 on_modify 可以很迅速的进行内存修改。此外不要忘记,on_modify 调用完了之后,仍需进一步更新 ini_entry->value,这样 EG(ini_directives)中的配置值就是最新的了。

3)这里出现了一张新的 hash 表,EG(modified_ini_directives)。

EG(modified_ini_directives)只用于存放被动态修改过的 ini 配置,如果一个 ini 配置被动态修改过,那么它既存在于 EG(ini_directives)中,又存在于 EG(modified_ini_directives)中。既然每一个 ini_entry 都有 modified 字段做标记,那岂不是可以遍历 EG(ini_directives)来获得所有被修改过的配置呢?

答案是肯定的。个人觉得,这里的 EG(modified_ini_directives)主要还是为了提升性能,酱直接遍历 EG(modified_ini_directives)就足够了。此外,把 EG(modified_ini_directives)的初始化推迟到 zend_alter_ini_entry_ex 中,也可以看出 php 在细节上的性能优化点。

2,恢复 配置

ini_set 的作用时间和 php.ini 文件的作用时间是不一样的,一旦请求执行结束,则 ini_set 会失效 。此外,当我们代码中调用了ini_restore 函数,则之前通过 ini_set 设置的配置也会失效。

每一个 php 请求执行完毕之后,会触发 php_request_shutdown,它和 php_request_startup 是两个相对应过程。如果 php 是挂接在 apache/nginx 下,则每处理完一个 http 请求,就会调用 php_request_shutdown;如果 php 以 CLI 模式来运行,则脚本执行完毕之后,也会调用 php_request_shutdown。

在 php_request_shutdown 中,我们可以看到针对 ini 的恢复处理:

/* 7. Shutdown scanner/executor/compiler and restore ini entries */
zend_deactivate(TSRMLS_C);

进入 zend_deactivate,可以进一步看到调用了 zend_ini_deactivate 函数,由 zend_ini_deactivate 来负责将 php 的配置进行恢复。

zend_try {zend_ini_deactivate(TSRMLS_C);
} zend_end_try();

具体来看看 zend_ini_deactivate 的实现:

ZEND_API int zend_ini_deactivate(TSRMLS_D) /* {{{*/
{if (EG(modified_ini_directives)) {// 遍历 EG(modified_ini_directives)中这张表
        // 对每一个 ini_entry 调用 zend_restore_ini_entry_wrapper
        zend_hash_apply(EG(modified_ini_directives), (apply_func_t) zend_restore_ini_entry_wrapper TSRMLS_CC);
        
        // 回收操作
        zend_hash_destroy(EG(modified_ini_directives));
        FREE_HASHTABLE(EG(modified_ini_directives));
        EG(modified_ini_directives) = NULL;
    }
    return SUCCESS;
}

从 zend_hash_apply 来看,真正恢复 ini 的任务最终落地到了 zend_restore_ini_entry_wrapper 回调函数。

static int zend_restore_ini_entry_wrapper(zend_ini_entry **ini_entry TSRMLS_DC)
{// zend_restore_ini_entry_wrapper 就是 zend_restore_ini_entry_cb 的封装
    zend_restore_ini_entry_cb(*ini_entry, ZEND_INI_STAGE_DEACTIVATE TSRMLS_CC);
    return 1;
}

static int zend_restore_ini_entry_cb(zend_ini_entry *ini_entry, int stage TSRMLS_DC)
{int result = FAILURE;

    // 只看修改过的 ini 项
    if (ini_entry->modified) {if (ini_entry->on_modify) {// 使用 orig_value,对 XXX_G 内的相关字段进行重新设置
            zend_try {result = ini_entry->on_modify(ini_entry, ini_entry->orig_value, ini_entry->orig_value_length, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage TSRMLS_CC);
            } zend_end_try();}
        if (stage == ZEND_INI_STAGE_RUNTIME && result == FAILURE) {/* runtime failure is OK */
            return 1;
        }
        if (ini_entry->value != ini_entry->orig_value) {efree(ini_entry->value);
        }
        
        // ini_entry 本身恢复到最原始的值
        ini_entry->value = ini_entry->orig_value;
        ini_entry->value_length = ini_entry->orig_value_length;
        ini_entry->modifiable = ini_entry->orig_modifiable;
        ini_entry->modified = 0;
        ini_entry->orig_value = NULL;
        ini_entry->orig_value_length = 0;
        ini_entry->orig_modifiable = 0;
    }
    return 0;
}

逻辑都蛮清晰的,相信读者可以看明白。总结一下关于 ini 配置的恢复流程:

php_request_shutdown—>zend_deactivate—>zend_ini_deactivate—>zend_restore_ini_entry_wrapper—>zend_restore_ini_entry_cb

3,配置的销毁

在 sapi 生命周期结束的时候,比如 apache 关闭,cli 程序执行完毕等等。一旦进入到这个阶段,之前所说的 configuration_hash,EG(ini_directives)等都需要被销毁,其用到的内存空间需要被释放。

1,php 会依次结束所有的模块,在每个模块的 PHP_MSHUTDOWN_FUNCTION 中调用 UNREGISTER_INI_ENTRIES。UNREGISTER_INI_ENTRIES 和 REGISTER_INI_ENTRIES 对应,但是 UNREGISTER_INI_ENTRIES 并不负责模块全局空间的释放,XXX_globals 这块内存放在静态数据区上,无需人为回收。

UNREGISTER_INI_ENTRIES 主要做的事情,是将某个模块的 ini_entry 配置从 EG(ini_directives)表中删除。删除之后,ini_entry 本身的空间会被回收,但是 ini_entry->value 不一定会被回收。

当所有模块的 PHP_MSHUTDOWN_FUNCTION 都调用 UNREGISTER_INI_ENTRIES 一遍之后,EG(ini_directives)中只剩下了 Core 模块的 ini 配置。此时,就需要手动调用 UNREGISTER_INI_ENTRIES,来完成对 Core 模块配置的删除工作。

void php_module_shutdown(TSRMLS_D)
{
    ...
    
    // zend_shutdown 会依次关闭除了 Core 之外的所有 php 模块
    // 关闭时会调用各个模块的 PHP_MSHUTDOWN_FUNCTION
    zend_shutdown(TSRMLS_C);
    
    ...

    // 至此,EG(ini_directives)中只剩下了 Core 模块的配置
    // 这里手动清理一下
    UNREGISTER_INI_ENTRIES();
    
    // 回收 configuration_hash
    php_shutdown_config();
    // 回收 EG(ini_directives)
zend_ini_shutdown(TSRMLS_C);
... }

当手动调用 UNREGISTER_INI_ENTRIES 完成之后,EG(ini_directives)已经不包含任何的元素,理论上讲,此时的 EG(ini_directives)是一张空的 hash 表。

2,configuration_hash 的回收发生在 EG(ini_directives)之后,上面贴出的代码中有关于 php_shutdown_config 的函数调用。php_shutdown_config 主要负责回收 configuration_hash。

int php_shutdown_config(void)
{// 回收 configuration_hash
    zend_hash_destroy(&configuration_hash);
    
    ...
    
    return SUCCESS;
}

注意 zend_hash_destroy 并不会释放 configuration_hash 本身的空间,同 XXX_G 访问的模块全局空间一样,configuration_hash 也是一个全局变量,无需手动回收。

3,当 php_shutdown_config 完成时,只剩下 EG(ini_directives)的自身空间还没被释放。因此最后一步调用 zend_ini_shutdown。zend_ini_shutdown 用于释放 EG(ini_directives)。在前文已经提到,此时的 EG(ini_directives)理论上是一张空的 hash 表,因此该 HashTable 本身所占用的空间需要被释放。

ZEND_API int zend_ini_shutdown(TSRMLS_D)
{// EG(ini_directives)是动态分配出的空间,需要回收
    zend_hash_destroy(EG(ini_directives));
    free(EG(ini_directives));
    return SUCCESS;
}

4,总结

用一张图大致描述一下和 ini 配置相关的流程:

 

深入理解 PHP 中的 ini 配置

本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-02/128442.htm

这篇文章不会详细叙述某个 ini 配置项的用途,这些在手册上已经讲解的面面俱到。我只是想从某个特定的角度去挖掘 php 的实现机制,会涉及到一些 php 内核方面的知识:-)

使用 php 的同学都知道 php.ini 配置的生效会贯穿整个 SAPI 的生命周期。在一段 php 脚本的执行过程中,如果手动修改 ini 配置,是不会启作用的。此时如果无法重启 apache 或者 nginx 等,那么就只能显式的在 php 代码中调用 ini_set 接口。ini_set是 php 向我们提供的一个动态修改配置的函数,需要注意的是,利用 ini_set 所设置的配置与 ini 文件中设置的配置,其生效的时间范围并不相同。在 php 脚本执行结束之后,ini_set 的设置便会随即失效。

因此本文打算分两篇,第一篇阐述 php.ini 配置原理,第二篇讲动态修改 php 配置。

php.ini 的配置大致会涉及到三块数据,configuration_hash,EG(ini_directives)以及 PG、BG、PCRE_G、JSON_G、XXX_G 等。如果不清楚这三种数据的含义也没有关系,下文会详细解释。

1,解析 INI 配置文件

由于 php.ini 需要在 SAPI 过程中一直生效,那么解析 ini 文件并据此来构建 php 配置的工作,必定是发生 SAPI 的一开始。换句话说,也就是必定发生在 php 的启动过程中。php 需要任意一个实际的请求到达之前,其内部已经生成好这些配置。

反映到 php 的内核,即为 php_module_startup 函数。

php_module_startup 主要负责对 php 进行启动,通常它会在 SAPI 开始的时候被调用。btw,还有一个常见的函数是 php_request_startup,它负责将在每个请求到来的时刻进行初始化,php_module_startup 与 php_request_startup 是两个标识性的动作,不过对他们进行分析并不在本文的探讨范围内。

举个例子,当 php 挂接在 apache 下面做一个 module,那么 apache 启动的时候,便会激活所有这些 module,其中包括 php module。在激活 php module 时,便会调用到 php_module_startup。php_module_startup 函数完成了茫茫多的工作,一旦 php_module_startup 调用结束就意味着,OK,php 已经启动,现在可以接受请求并作出响应了。

在 php_module_startup 函数中,与解析 ini 文件相关的实现是:

/* this will read in php.ini, set up the configuration parameters,
   load zend extensions and register php function extensions
   to be loaded later */
if (php_init_config(TSRMLS_C) == FAILURE) {return FAILURE;
}

可以看到,其实就是调用了 php_init_config 函数,去完成对 ini 文件的 parse。parse 工作主要进行 lex&grammar 分析,并将 ini 文件中的 key、value 键值对提取出来并保存。php.ini 的格式很简单,等号左侧为 key,右侧为 value。每当一对 kv 被提取出来之后,php 将它们存储到哪儿呢?答案就是之前提到的 configuration_hash。

static HashTable configuration_hash;

configuration_hash 声明在 php_ini.c 中,它是一个 HashTable 类型的数据结构。顾名思义,其实就是张 hash 表。题外话,在 php5.3 之前的版本是没法获取 configuration_hash 的,因为它是 php_ini.c 文件的一个 static 的变量。后来 php5.3 添加了 php_ini_get_configuration_hash 接口,该接口直接返回 &configuration_hash,使 得 php 各个扩展可以方便的一窥 configuration_hash 全貌 … 真是普大喜奔 …

注意四点:

第一,php_init_config 不会做除了词法语法以外的任何校验。也就是说,假如我们在 ini 文件中添加一行 hello=world,只要这是一个格式正确的配置项,那么最终 configuration_hash 中就会包含一个键为 hello、值为 world 的元素,configuration_hash 最大限度的反映出 ini 文件。

第二,ini 文件允许我们以数组的形式进行配置。例如 ini 文件中写入以下三行:

drift.arr[]=1
drift.arr[]=2
drift.arr[]=3

那么最终生成的 configuration_hash 表中,就会存在一个 key 为 drift.arr 的元素,其 value 为一个包含的 1,2,3 三个数字的数组。这是一种极为罕见的配置方法。

第三,php 还允许我们除了默认的 php.ini 文件(准确说是 php-%s.ini)之外,另外构建一些 ini 文件。这些 ini 文件会被放入一个额外的目录。该目录由环境变量 PHP_INI_SCAN_DIR 来指定,当 php_init_config 解析完了 php.ini 之后,会再次扫描此目录,然后找出目录中所有.ini 文件来分析。这些额外的 ini 文件中产生的 kv 键值对,也会被加入到 configuration_hash 中去。

这是一个偶尔有用的特性,假设我们自己开发 php 的扩展,却又不想将配置混入 php.ini,便可以另外写一份 ini,并通过 PHP_INI_SCAN_DIR 告诉 php 该去哪儿找到它。当然,其缺点也显而易见,其需要设置额外的环境变量来支持。更好的解决办法是,开发者在扩展中自己调用 php_parse_user_ini_file 或 zend_parse_ini_file 去解析对应的 ini 文件。

第四,在 configuration_hash 中,key 是字符串,那么值的类型是什么?答案也是字符串(除了上述很特殊的数组)。具体来说,比如下面的配置:

display_errors = On
log_errors = Off
log_errors_max_len = 1024

 那么最后 configuration_hash 中实际存放的键值对为:

key: "display_errors"
val : "1"

key: "log_errors"
val : ""

key: "log_errors_max_len"
val : "1024"

注意 log_errors,其存放的值连 ”0″ 都不是,就是一个实实在在地空字符串。另外,log_errors_max_len 也并非数字,而是字符串 1024。

分析至此,基本上解析 ini 文件相关的内容都说清楚了。简单总结一下:

1,解析 ini 发生在 php_module_startup 阶段

2,解析结果存放在 configuration_hash 里。

2,配置作用到模块

php 的大致结构可以看成是最下层有一个 zend 引擎,它负责与 OS 进行交互、编译 php 代码、提供内存托管等等,在 zend 引擎的上层,排列着很多很多的模块。其中最核心的就一个 Core 模块,其他还有比如 Standard,PCRE,Date,Session 等等 … 这些模块还有另一个名字叫 php 扩展。我们可以简单理解为,每个模块都会提供一组功能接口给开发者来调用,举例来说,常用的诸如 explode,trim,array 等内置函数,便是由 Standard 模块提供的。

为什么需要谈到这些,是因为在 php.ini 里除了针对 php 自身,也就是针对 Core 模块的一些配置(例如 safe_mode,display_errors,max_execution_time 等),还有相当多的配置是针对其他不同模块的。

例如,date 模块,它提供了常见的 date,time,strtotime 等函数。在 php.ini 中,它的相关配置形如:

[Date]
;date.timezone = 'Asia/Shanghai'
;date.default_latitude = 31.7667
;date.default_longitude = 35.2333
;date.sunrise_zenith = 90.583333
;date.sunset_zenith = 90.583333

除了这些模块拥有独立的配置,zend 引擎也是可配的,只不过 zend 引擎的可配项非常少,只有 error_reporting,zend.enable_gc 和 detect_unicode 三项。

在上一小节中我们已经谈到,php_module_startup 会调用 php_init_config,其目的是解析 ini 文件并生成 configuration_hash。那么接下来在 php_module_startup 中还会做什么事情呢?很显然,就是会将 configuration_hash 中的配置作用于 Zend,Core,Standard,SPL 等不同模块。当然这并非一个一蹴而就的过程,因为 php 通常会包含有很多模块,php 启动的过程中这些模块也会依次进行启动。那么,对模块 A 进行配置的过程,便是发生在模块 A 的启动过程中。

有扩展开发经验的同学会直接指出,模块 A 的启动不就是在 PHP_MINIT_FUNCTION(A)中么?

是的,如果模块 A 需要配置,那么在 PHP_MINIT_FUNCTION 中,可以调用 REGISTER_INI_ENTRIES()来完成。REGISTER_INI_ENTRIES 会根据当前模块所需要的配置项名称,去 configuration_hash 查找用户设置的配置值,并更新到模块自己的全局空间中。

2.1,模块的全局空间

要理解如何将 ini 配置从 configuration_hash 作用到各个模块之前,有必要先了解一下 php 模块的全局空间。对于不同的 php 模块,均可以开辟一块属于自己的存储空间,并且这块空间对于该模块来说,是全局可见的。一般而言,它会被用来存放该模块所需的 ini 配置。也就是说,configuration_hash 中的配置项,最终会被存放到该全局空间中。在模块的执行过程中,只需要直接访问这块全局空间,就可以拿到用户针对该模块进行的设置。当然,它也经常被用来记录模块在执行过程中的中间数据。

我们以 bcmath 模块来举例说明,bcmath 是一个提供数学计算方面接口的 php 模块,首先我们来看看它有哪些 ini 配置:

PHP_INI_BEGIN()
    STD_PHP_INI_ENTRY("bcmath.scale", "0", PHP_INI_ALL, OnUpdateLongGEZero, bc_precision, zend_bcmath_globals, bcmath_globals)
PHP_INI_END()

bcmath 只有一个配置项,我们可以在 php.ini 中用bcmath.scale 来配置bcmath 模块。

接下来继续看看 bcmatch 模块的全局空间定义。在 php_bcmath.h 中有如下声明:

ZEND_BEGIN_MODULE_GLOBALS(bcmath)
   bc_num _zero_;
   bc_num _one_;
   bc_num _two_;
   long bc_precision;
ZEND_END_MODULE_GLOBALS(bcmath)

 宏展开之后,即为:

typedef struct _zend_bcmath_globals {
    bc_num _zero_;
    bc_num _one_;
    bc_num _two_;
    long bc_precision;
} zend_bcmath_globals;

其实,zend_bcmath_globals 类型 就是 bcmath 模块中的全局空间类型。这里仅仅声明了zend_bcmath_globals 结构体,在bcmath.c 中还有具体的实例化定义:

// 展开后即为 zend_bcmath_globals bcmath_globals;
ZEND_DECLARE_MODULE_GLOBALS(bcmath)

可以看出,用 ZEND_DECLARE_MODULE_GLOBALS 完成了对变量 bcmath_globals 的定义。

bcmath_globals 是一块真正的全局空间,它包含有四个字段。其最后一个字段 bc_precision,对应于 ini 配置中的 bcmath.scale。我们在 php.ini 中设置了bcmath.scale 的值,随后在启动 bcmath 模块的时候,bcmath.scale 的值 被更新到 bcmath_globals.bc_precision 中去。

把 configuration_hash 中的值,更新到各个模块自己定义的 xxx_globals 变量中,就是所谓的将 ini 配置作用到模块。一旦模块启动完成,那么这些配置也都作用到位。所以 在随后的执行阶段,php 模块无需再次访问 configuration_hash,模块仅需要访问自己的 XXX_globals,就可以拿到用户设定的配置。

bcmath_globals,除了有一个字段为 ini 配置项,其他还有三个字段为何意?这就是模块全局空间的第二个作用,它除了用于 ini 配置,还可以存储模块执行过程中的一些数据。

深入理解 PHP 中的 ini 配置

再例如 json 模块,也是 php 中一个很常用的模块:

ZEND_BEGIN_MODULE_GLOBALS(json)
    int error_code;
ZEND_END_MODULE_GLOBALS(json)

可以看到 json 模块并不需要 ini 配置,它的全局空间只有一个字段 error_code。error_code 记录了上一次执行 json_decode 或者 json_encode 中发生的错误。json_last_error 函数便是返回这个 error_code,来帮助用户定位错误原因。

为了能够很便捷的访问模块全局空间变量,php 约定俗成的提出了一些宏。比如我们想访问 json_globals 中的 error_code,当然可以直接写做 json_globals.error_code(多线程环境下不行),不过更通用的写法是定义 JSON_G 宏:

#define JSON_G(v) (json_globals.v)

我们使用 JSON_G(error_code)来访问 json_globals.error_code。本文刚开始的时候,曾提到 PG、BG、JSON_G、PCRE_G,XXX_G 等等,这些宏在 php 源代码中也是很常见的。现在我们可以很轻松的理解它们,PG 宏可以访问 Core 模块的全局变量,BG 访问 Standard 模块的全局变量,PCRE_G 则访问 PCRE 模块的全局变量。

#define PG(v) (core_globals.v)
#define BG(v) (basic_globals.v)

2.2,如何确定一个模块需要哪些配置呢?

模块需要什么样的 INI 配置,都是在各个模块中自己定义的。举例来说,对于 Core 模块,有如下的配置项定义:

PHP_INI_BEGIN()
    ......
    STD_PHP_INI_ENTRY_EX("display_errors", "1", PHP_INI_ALL,    OnUpdateDisplayErrors, display_errors, php_core_globals, core_globals, display_errors_mode)
    STD_PHP_INI_BOOLEAN("enable_dl",       "1", PHP_INI_SYSTEM, OnUpdateBool,          enable_dl,      php_core_globals, core_globals)
    STD_PHP_INI_BOOLEAN("expose_php",      "1", PHP_INI_SYSTEM, OnUpdateBool,          expose_php,     php_core_globals, core_globals)
    STD_PHP_INI_BOOLEAN("safe_mode",       "0", PHP_INI_SYSTEM, OnUpdateBool,          safe_mode,      php_core_globals, core_globals)
    ......
PHP_INI_END()

可以在 php-src\main\main.c 文件大概 450+ 行找到上述代码。其中涉及的宏比较多,有 ZEND_INI_BEGIN、ZEND_INI_END、PHP_INI_ENTRY_EX、STD_PHP_INI_BOOLEAN等等,本文不一一赘述,感兴趣的读者可自行分析。

上述代码进行宏展开后得到:

static const zend_ini_entry ini_entries[] = {
    ..
    {0, PHP_INI_ALL,    "display_errors",sizeof("display_errors"),OnUpdateDisplayErrors,(void *)XtOffsetOf(php_core_globals, display_errors), (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, display_errors_mode },
    {0, PHP_INI_SYSTEM, "enable_dl",     sizeof("enable_dl"),     OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, enable_dl),      (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    {0, PHP_INI_SYSTEM, "expose_php",    sizeof("expose_php"),    OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, expose_php),     (void *)&core_globals, NULL, "1", sizeof("1")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    {0, PHP_INI_SYSTEM, "safe_mode",     sizeof("safe_mode"),     OnUpdateBool,         (void *)XtOffsetOf(php_core_globals, safe_mode),      (void *)&core_globals, NULL, "0", sizeof("0")-1, NULL, 0, 0, 0, zend_ini_boolean_displayer_cb },
    ...
    {0, 0, NULL, 0, NULL, NULL, NULL, NULL, NULL, 0, NULL, 0, 0, 0, NULL }
};

我们看到,配置项的定义,其本质上就是定义了一个 zend_ini_entry 类型的数组。zend_ini_entry 结构体的字段具体含义为:

struct _zend_ini_entry {int module_number;                // 模块的 id
    int modifiable;                   // 可被修改的范围,例如 php.ini,ini_set
    char *name;                       // 配置项的名称
    uint name_length;
    ZEND_INI_MH((*on_modify));        // 回调函数,配置项注册或修改的时候会调用
    void *mh_arg1;                    // 通常为配置项字段在 XXX_G 中的偏移量
    void *mh_arg2;                    // 通常为 XXX_G
    void *mh_arg3;                    // 通常为保留字段,极少用到

    char *value;                      // 配置项的值
    uint value_length;

    char *orig_value;                 // 配置项的原始值
    uint orig_value_length;
    int orig_modifiable;              // 配置项的原始 modifiable
    int modified;                     // 是否发生过修改,如果有修改,则 orig_value 会保存修改前的值

    void (*displayer)(zend_ini_entry *ini_entry, int type);
};

2.3,将配置作用到模块——REGISTER_INI_ENTRIES

经常能够在不同扩展的 PHP_MINIT_FUNCTION 里看到 REGISTER_INI_ENTRIES。REGISTER_INI_ENTRIES 主要负责完成两件事情,第一,对模块的全局空间 XXX_G 进行填充,同步 configuration_hash 中的值 到 XXX_G 中去。其次,它还生成了 EG(ini_directives)。

REGISTER_INI_ENTRIES 也是一个宏,展开之后实则为 zend_register_ini_entries 方法。具体来看下 zend_register_ini_entries 的实现:

ZEND_API int zend_register_ini_entries(const zend_ini_entry *ini_entry, int module_number TSRMLS_DC) /* {{{*/
{// ini_entry 为 zend_ini_entry 类型数组,p 为数组中每一项的指针
    const zend_ini_entry *p = ini_entry;
    zend_ini_entry *hashed_ini_entry;
    zval default_value;
    
    // EG(ini_directives)就是 registered_zend_ini_directives
    HashTable *directives = registered_zend_ini_directives;
    zend_bool config_directive_success = 0;
    
    // 还记得 ini_entry 最后一项固定为 {0, 0, NULL, ...} 么
    while (p->name) {config_directive_success = 0;
        
        // 将 p 指向的 zend_ini_entry 加入 EG(ini_directives)
        if (zend_hash_add(directives, p->name, p->name_length, (void*)p, sizeof(zend_ini_entry), (void **) &hashed_ini_entry) == FAILURE) {zend_unregister_ini_entries(module_number TSRMLS_CC);
            return FAILURE;
        }
        hashed_ini_entry->module_number = module_number;
        
        // 根据 name 去 configuration_hash 中查询,取出来的结果放在 default_value 中
        // 注意 default_value 的值比较原始,一般是数字、字符串、数组等,具体取决于 php.ini 中的写法
        if ((zend_get_configuration_directive(p->name, p->name_length, &default_value)) == SUCCESS) {// 调用 on_modify 更新到模块的全局空间 XXX_G 中
            if (!hashed_ini_entry->on_modify || hashed_ini_entry->on_modify(hashed_ini_entry, Z_STRVAL(default_value), Z_STRLEN(default_value), hashed_ini_entry->mh_arg1, hashed_ini_entry->mh_arg2, hashed_ini_entry->mh_arg3, ZEND_INI_STAGE_STARTUP TSRMLS_CC) == SUCCESS) {hashed_ini_entry->value = Z_STRVAL(default_value);
                hashed_ini_entry->value_length = Z_STRLEN(default_value);
                config_directive_success = 1;
            }
        }

        // 如果 configuration_hash 中没有找到,则采用默认值
        if (!config_directive_success && hashed_ini_entry->on_modify) {hashed_ini_entry->on_modify(hashed_ini_entry, hashed_ini_entry->value, hashed_ini_entry->value_length, hashed_ini_entry->mh_arg1, hashed_ini_entry->mh_arg2, hashed_ini_entry->mh_arg3, ZEND_INI_STAGE_STARTUP TSRMLS_CC);
        }
        p++;
    }
    return SUCCESS;
}

简单来说,可以把上述代码的逻辑表述为:

1,将模块声明的 ini 配置项添加到 EG(ini_directives)中。注意,ini 配置项的值可能在随后被修改。

2,尝试去 configuration_hash 中寻找各个模块需要的 ini。

  • 如果能够找到,说明用户叜 ini 文件中配置了该值,那么采用用户的配置。
  • 如果没有找到,OK,没有关系,因为模块在声明 ini 的时候,会带上默认值。

3,将 ini 的值同步到 XX_G 里面。毕竟在 php 的执行过程中,起作用的还是这些 XXX_globals。具体的过程是调用每条 ini 配置对应的 on_modify 方法完成,on_modify 由模块在声明 ini 的时候进行指定。

我们来具体看下 on_modify,它其实是一个函数指针,来看两个具体的 Core 模块的配置声明:

STD_PHP_INI_BOOLEAN("log_errors",      "0",    PHP_INI_ALL, OnUpdateBool, log_errors,         php_core_globals, core_globals)
STD_PHP_INI_ENTRY("log_errors_max_len","1024", PHP_INI_ALL, OnUpdateLong, log_errors_max_len, php_core_globals, core_globals)

对于 log_errors,它的 on_modify 被设置为 OnUpdateBool,对于 log_errors_max_len,则 on_modify 被设置为 OnUpdateLong。

进一步假设我们在 php.ini 中的配置为:

log_errors = On
log_errors_max_len = 1024

具体来看下 OnUpdateBool 函数:

ZEND_API ZEND_INI_MH(OnUpdateBool) 
{zend_bool *p;
    
    // base 表示 core_globals 的地址
    char *base = (char *) mh_arg2;

    // p 表示 core_globals 的地址加上 log_errors 字段的偏移量
    // 得到的即为 log_errors 字段的地址
    p = (zend_bool *) (base+(size_t) mh_arg1);  

    if (new_value_length == 2 && strcasecmp("on", new_value) == 0) {*p = (zend_bool) 1;
    }
    else if (new_value_length == 3 && strcasecmp("yes", new_value) == 0) {*p = (zend_bool) 1;
    }
    else if (new_value_length == 4 && strcasecmp("true", new_value) == 0) {*p = (zend_bool) 1;
    }
    else {// configuration_hash 中存放的 value 是字符串 "1",而非 "On"
        // 因此这里用 atoi 转化成数字 1 
        *p = (zend_bool) atoi(new_value);
    }
    return SUCCESS;
}

最令人费解的估计就是 mh_arg1 和 mh_arg2 了,其实对照前面所述的 zend_ini_entry 定义,mh_arg1,mh_arg2 还是很容易参透的。mh_arg1 表示字节偏移量,mh_arg2表示 XXX_globals 的地址。因此,(char *)mh_arg2 + mh_arg1 的结果即为 XXX_globals 中某个字段的地址。具体到本 case 中,就是计算 core_globals中 log_errors 的地址。因此,当 OnUpdateBool 最后执行到

*p = (zend_bool) atoi(new_value);

其作用就相当于

core_globals.log_errors(zend_bool) atoi(“1”);

分析完了OnUpdateBool,我们再来看OnUpdateLong 便觉得一目了然:

ZEND_API ZEND_INI_MH(OnUpdateLong)
{long *p;
    char *base = (char *) mh_arg2;

    // 获得 log_errors_max_len 的地址
    p = (long *) (base+(size_t) mh_arg1);

    // 将 "1024" 转化成 long 型,并赋值给 core_globals.log_errors_max_len
    *p = zend_atol(new_value, new_value_length);
    return SUCCESS;
}

最后需要注意的是,zend_register_ini_entries 函数中,如果 configuration_hash 中存在配置,则当调用 on_modify 结束后,hashed_ini_entry 中的 value 和 value_length 会被更新。也就是说,如果用户在 php.ini 中配置过,则 EG(ini_directives)存放的就是实际配置的值。如果用户没配,EG(ini_directives)中存放的是声明 zend_ini_entry 时给出的默认值。

zend_register_ini_entries 中的 default_value 变量命名比较糟糕,相当容易造成误解。其实 default_value 并非表示默认值,而是表示用户实际配置的值。

3,总结

至此,三块数据 configuration_hash,EG(ini_directives)以及 PG、BG、PCRE_G、JSON_G、XXX_G… 已经都交代清楚了。

总结一下:

1,configuration_hash,存放 php.ini 文件里的配置,不做校验,其值为字符串。
2,EG(ini_directives),存放的是各个模块中定义的 zend_ini_entry,如果用户在 php.ini 配置过(configuration_hash 中存在),则值被替换为 configuration_hash 中的值,类型依然是字符串。
3,XXX_G,该宏用于访问模块的全局空间,这块内存空间可用来存放 ini 配置,并通过 on_modify 指定的函数进行更新,其数据类型由 XXX_G 中的字段声明来决定。

更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2016-02/128442p2.htm

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计34123字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19350
评论数
4
阅读量
7960424
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
星哥带你玩飞牛NAS-14:解锁公网自由!Lucky功能工具安装使用保姆级教程

星哥带你玩飞牛NAS-14:解锁公网自由!Lucky功能工具安装使用保姆级教程

星哥带你玩飞牛 NAS-14:解锁公网自由!Lucky 功能工具安装使用保姆级教程 作为 NAS 玩家,咱们最...
CSDN,你是老太太喝粥——无齿下流!

CSDN,你是老太太喝粥——无齿下流!

CSDN,你是老太太喝粥——无齿下流! 大家好,我是星哥,今天才思枯竭,不写技术文章了!来吐槽一下 CSDN。...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

还在忍受动辄数百兆的“全家桶”监控软件?后台偷占资源、界面杂乱冗余,想查个 CPU 温度都要层层点选? 今天给...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
星哥带你玩飞牛NAS-14:解锁公网自由!Lucky功能工具安装使用保姆级教程

星哥带你玩飞牛NAS-14:解锁公网自由!Lucky功能工具安装使用保姆级教程

星哥带你玩飞牛 NAS-14:解锁公网自由!Lucky 功能工具安装使用保姆级教程 作为 NAS 玩家,咱们最...
安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装 Black 群晖 DSM7.2 系统安装教程(在 Vmware 虚拟机中、实体机均可)! 前言 大家好,...
国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号 AI 知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率 大家好,我是星哥,...
仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

还在忍受动辄数百兆的“全家桶”监控软件?后台偷占资源、界面杂乱冗余,想查个 CPU 温度都要层层点选? 今天给...
让微信公众号成为 AI 智能体:从内容沉淀到智能问答的一次升级

让微信公众号成为 AI 智能体:从内容沉淀到智能问答的一次升级

让微信公众号成为 AI 智能体:从内容沉淀到智能问答的一次升级 大家好,我是星哥,之前写了一篇文章 自己手撸一...