PHP內核之動態修改PHP.INI

運行時改變配置

在前一篇中曾經談到,ini_set函數可以在php執行的過程中,動態修改php的部分配置。注意,僅僅是部分,並非所有的配置都可以動態修改。關於ini配置的可修改性,參見:http://php.net/manual/zh/configuration.changes.modes.php

我們直接進入ini_set的實現,函數雖然有點長,但是邏輯很清晰:

/* {{{ proto string ini_set(string varname, string newvalue)
   Set a configuration option, returns false on error and the old value of the configuration option on success */
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))
	/* open basedir check */
	if (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 (php_check_open_basedir(new_value TSRMLS_CC)) {
				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) { /* we already changed the value, free the changed 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在細節上的性能優化點。

恢復配置

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 {
			/* even if on_modify bails out, we have to continue on with restoring,
				since there can be allocated variables that would be freed on MM shutdown
				and would lead to memory corruption later ini entry is modified again */
				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

配置的銷燬

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

/* {{{ php_shutdown_config
 */
int php_shutdown_config(void)
{
	zend_hash_destroy(&configuration_hash);		// 回收configuration_hash
	if (php_ini_opened_path) {
		free(php_ini_opened_path);
		php_ini_opened_path = NULL;
	}
	if (php_ini_scanned_files) {
		free(php_ini_scanned_files);
		php_ini_scanned_files = NULL;
	}
	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;
}
/* }}} */

總結

用一張圖大致描述一下和ini配置相關的流程:





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章