深刻理解php中的ini配置(1)

這篇文章不會詳細敘述某個ini配置項的用途,這些在手冊上已經講解的面面俱到。我只是想從某個特定的角度去挖掘php的實現機制,會涉及到一些php內核方面的知識:-)php

使用php的同窗都知道php.ini配置的生效會貫穿整個SAPI的生命週期。在一段php腳本的執行過程當中,若是手動修改ini配置,是不會啓做用的。此時若是沒法重啓apache或者nginx等,那麼就只能顯式的在php代碼中調用ini_set接口。ini_set是php向咱們提供的一個動態修改配置的函數,須要注意的是,利用ini_set所設置的配置與ini文件中設置的配置,其生效的時間範圍並不相同。在php腳本執行結束以後,ini_set的設置便會隨即失效。nginx

所以本文打算分兩篇,第一篇闡述php.ini配置原理,第二篇講動態修改php配置。git

php.ini的配置大體會涉及到三塊數據,configuration_hash,EG(ini_directives)以及PG、BG、PCRE_G、JSON_G、XXX_G等。若是不清楚這三種數據的含義也沒有關係,下文會詳細解釋。apache

解析INI配置文件

因爲php.ini須要在SAPI過程當中一直生效,那麼解析ini文件並據此來構建php配置的工做,一定是發生SAPI的一開始。換句話說,也就是一定發生在php的啓動過程當中。php須要任意一個實際的請求到達以前,其內部已經生成好這些配置。json

反映到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。ui

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裏。

配置做用到模塊

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查找用戶設置的配置值,並更新到模塊本身的全局空間中。

模塊的全局空間

要理解如何將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配置,還能夠存儲模塊執行過程當中的一些數據。

再例如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)

如何肯定一個模塊須要哪些配置呢?

模塊須要什麼樣的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);
};

將配置做用到模塊——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並不是表示默認值,而是表示用戶實際配置的值。

總結

至此,三塊數據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中的字段聲明來決定。

相關文章
相關標籤/搜索