使用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檔案相關的實作是:
可以看到,其實就是呼叫了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檔案中寫入以下三行:
那麼最終產生的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是字串,那麼值的型別是什麼?答案也是字串(除了上述很特殊的陣列)。具體來說,例如下面的配置:
則最後configuration_hash中實際存放的鍵值對為:
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中,它的相關配置形如:
除了這些模組擁有獨立的配置,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配置:
bcmath只有一個配置項,我們可以在php.ini中用bcmath.scale來設定bcmath模組。
接下來繼續看看bcmatch模組的全域空間定義。在php_bcmath.h中有以下聲明:
宏展開後,即為:
其實,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中一個很常用的模組:
可以看到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巨集:
我們使用JSON_G(error_code)來存取json_globals.error_code。本文剛開始的時候,曾提到PG、BG、JSON_G、PCRE_G,XXX_G等等,這些巨集在php原始碼中也是很常見的。現在我們可以很輕鬆的理解它們,PG宏可以存取Core模組的全域變量,BG存取Standard模組的全域變量,PCRE_G則存取PCRE模組的全域變數。
2.2,如何決定一個模組需要哪些配置?
模組需要什麼樣的INI配置,都是在各個模組中自己定義的。舉例來說,對於Core模組,有以下的配置項定義:
可以在php-src\main\main.c檔案大概450 行找到上述程式碼。其中涉及的巨集比較多,有ZEND_INI_BEGIN 、ZEND_INI_END、PHP_INI_ENTRY_EX、STD_PHP_INI_BOOLEAN等等,本文不一一贅述,有興趣的讀者可自行分析。
上述程式碼進行巨集展開後得到:
我們看到,配置項的定義,其本質上就是定義了一個zend_ini_entry類型的集群。zend_ini_entry結構體的字段具體含義為:
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的實作:
// 若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_ent的h_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模組的設定宣告:
對於log_errors,它的on_modify被設定為OnUpdateBool,對於log_errors_max_len,則on_modify被設定為OnUpdateLong。
進一步假設我們在php.ini的配置為:
具體來看下OnUpdateBool函數:
// 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最後執行到
其作用就相當於
分析完了OnUpdateBool,再來看OnUpdateLong便覺得一目了然:
// 取得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中的欄位聲明來決定。