在PHP腳本執行的時候,用戶全局變量(在用戶空間顯式定義的變量)會保存在一個HashTable數據類型的符號表(symbol_table)中,而我們用得非常多的在全局范圍內有效的變量卻與這些用戶全局變量不同。例如:$_GET,$_POST,$_SERVER,$_FILES等變量,我們并沒有在程序中定義這些變量,并且這些變量也同樣保存在符號表中,從這些表象我們不難得出結論:PHP是在腳本運行之前就將這些特殊的變量加入到了符號表。
## 預定義變量$GLOBALS的初始化
我們以cgi模式為例說明$GLOBALS的初始化。從cgi_main.c文件main函數開始。整個調用順序如下所示:
**[main() -> php_request_startup() -> zend_activate() -> init_executor() ]**
... // 省略
zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);
{
zval *globals;
?
ALLOC_ZVAL(globals);
Z_SET_REFCOUNT_P(globals, 1);
Z_SET_ISREF_P(globals);
Z_TYPE_P(globals) = IS_ARRAY;
Z_ARRVAL_P(globals) = &EG(symbol_table);
zend_hash_update(&EG(symbol_table), "GLOBALS", sizeof("GLOBALS"),
&globals, sizeof(zval *), NULL); // 添加全局變量$GLOBALS
}
... // 省略
php_request_startup函數在PHP的生命周期中屬于請求初始化階段,即每個請求都會執行這個函數。因此,對于每個用戶請求,其用到的這些預定義的全局變量都會不同。$GLOVALS的關鍵點在于zend_hash_update函數的調用,它將變量名為GLOBALS的變量注冊到EG(symbol_table)中,EG(symbol_table)是一個HashTable的結構,用來存放頂層作用域的變量。通過這個操作,GLOBAL變量與其它頂層的變量一樣都會注冊到了變量表,也可以和其它變量一樣直接訪問了。這在下面將要提到的$_GET等變量初始化時也會用到。
## $_GET、$_POST等變量的初始化
**$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES、$_REQUEST**這六個變量都是通過如下的調用序列進行初始化。**[main() -> php_request_startup() -> php_hash_environment() ]**
在請求初始化時,通過調用 **php_hash_environment** 函數初始化以上的六個預定義的變量。如下所示為php_hash_environment函數的代碼。在代碼之后我們以$_POST為例說明整個初始化的過程。
/* {{{ php_hash_environment */
int php_hash_environment(TSRMLS_D)
{
char *p;
unsigned char _gpc_flags[5] = {0, 0, 0, 0, 0};
zend_bool jit_initialization = (PG(auto_globals_jit) && !PG(register_globals) && !PG(register_long_arrays));
struct auto_global_record {
char *name;
uint name_len;
char *long_name;
uint long_name_len;
zend_bool jit_initialization;
} auto_global_records[] = {
{ "_POST", sizeof("_POST"), "HTTP_POST_VARS", sizeof("HTTP_POST_VARS"), 0 },
{ "_GET", sizeof("_GET"), "HTTP_GET_VARS", sizeof("HTTP_GET_VARS"), 0 },
{ "_COOKIE", sizeof("_COOKIE"), "HTTP_COOKIE_VARS", sizeof("HTTP_COOKIE_VARS"), 0 },
{ "_SERVER", sizeof("_SERVER"), "HTTP_SERVER_VARS", sizeof("HTTP_SERVER_VARS"), 1 },
{ "_ENV", sizeof("_ENV"), "HTTP_ENV_VARS", sizeof("HTTP_ENV_VARS"), 1 },
{ "_FILES", sizeof("_FILES"), "HTTP_POST_FILES", sizeof("HTTP_POST_FILES"), 0 },
};
size_t num_track_vars = sizeof(auto_global_records)/sizeof(struct auto_global_record);
size_t i;
?
/* jit_initialization = 0; */
for (i=0; i<num_track_vars; i++) {
PG(http_globals)[i] = NULL;
}
?
for (p=PG(variables_order); p && *p; p++) {
switch(*p) {
case 'p':
case 'P':
if (!_gpc_flags[0] && !SG(headers_sent) && SG(request_info).request_method && !strcasecmp(SG(request_info).request_method, "POST")) {
sapi_module.treat_data(PARSE_POST, NULL, NULL TSRMLS_CC); /* POST Data */
_gpc_flags[0] = 1;
if (PG(register_globals)) {
php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_POST]) TSRMLS_CC);
}
}
break;
case 'c':
case 'C':
if (!_gpc_flags[1]) {
sapi_module.treat_data(PARSE_COOKIE, NULL, NULL TSRMLS_CC); /* Cookie Data */
_gpc_flags[1] = 1;
if (PG(register_globals)) {
php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_COOKIE]) TSRMLS_CC);
}
}
break;
case 'g':
case 'G':
if (!_gpc_flags[2]) {
sapi_module.treat_data(PARSE_GET, NULL, NULL TSRMLS_CC); /* GET Data */
_gpc_flags[2] = 1;
if (PG(register_globals)) {
php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_GET]) TSRMLS_CC);
}
}
break;
case 'e':
case 'E':
if (!jit_initialization && !_gpc_flags[3]) {
zend_auto_global_disable_jit("_ENV", sizeof("_ENV")-1 TSRMLS_CC);
php_auto_globals_create_env("_ENV", sizeof("_ENV")-1 TSRMLS_CC);
_gpc_flags[3] = 1;
if (PG(register_globals)) {
php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_ENV]) TSRMLS_CC);
}
}
break;
case 's':
case 'S':
if (!jit_initialization && !_gpc_flags[4]) {
zend_auto_global_disable_jit("_SERVER", sizeof("_SERVER")-1 TSRMLS_CC);
php_register_server_variables(TSRMLS_C);
_gpc_flags[4] = 1;
if (PG(register_globals)) {
php_autoglobal_merge(&EG(symbol_table), Z_ARRVAL_P(PG(http_globals)[TRACK_VARS_SERVER]) TSRMLS_CC);
}
}
break;
}
}
?
/* argv/argc support */
if (PG(register_argc_argv)) {
php_build_argv(SG(request_info).query_string, PG(http_globals)[TRACK_VARS_SERVER] TSRMLS_CC);
}
?
for (i=0; i<num_track_vars; i++) {
if (jit_initialization && auto_global_records[i].jit_initialization) {
continue;
}
if (!PG(http_globals)[i]) {
ALLOC_ZVAL(PG(http_globals)[i]);
array_init(PG(http_globals)[i]);
INIT_PZVAL(PG(http_globals)[i]);
}
?
Z_ADDREF_P(PG(http_globals)[i]);
zend_hash_update(&EG(symbol_table), auto_global_records[i].name, auto_global_records[i].name_len, &PG(http_globals)[i], sizeof(zval *), NULL);
if (PG(register_long_arrays)) {
zend_hash_update(&EG(symbol_table), auto_global_records[i].long_name, auto_global_records[i].long_name_len, &PG(http_globals)[i], sizeof(zval *), NULL);
Z_ADDREF_P(PG(http_globals)[i]);
}
}
?
/* Create _REQUEST */
if (!jit_initialization) {
zend_auto_global_disable_jit("_REQUEST", sizeof("_REQUEST")-1 TSRMLS_CC);
php_auto_globals_create_request("_REQUEST", sizeof("_REQUEST")-1 TSRMLS_CC);
}
?
return SUCCESS;
}
以$_POST為例,首先以 **auto_global_record** 數組形式定義好將要初始化的變量的相關信息。在變量初始化完成后,按照PG(variables_order)指定的順序(在php.ini中指定),通過調用sapi_module.treat_data處理數據。
> 從PHP實現的架構設計看,treat_data函數在SAPI目錄下不同的服務器應該有不同的實現,只是現在大部分都是使用的默認實現。
在treat_data后,如果打開了PG(register_globals),則會調用php_autoglobal_merge將相關變量的值寫到符號表。
以上的所有數據處理是一個賦值前的初始化行為。在此之后,通過遍歷之前定義的結構體,調用zend_hash_update,將相關變量的值賦值給&EG(symbol_table)。另外對于$_REQUEST有獨立的處理方法。
以文件上傳中獲取文件的信息為例(假設在Apache服務器環境下):我們首先創建一個靜態頁面test.html,其內容如下所示:
<form name="upload" action="upload_test.php" method="POST" enctype="multipart/form-data">
<input type="hidden" value="1024" name="MAX_FILE_SIZE" />
請選擇文件:<input name="ufile" type="file" />
<input type="submit" value="提 交" />
</form>
當我們在頁面中選擇點擊提交按鈕時,瀏覽器會將數據提交給服務器。通過Filddle我們可以看到其提交的請求頭如下:
POST http://localhost/test/upload_test.php HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 1347
Cache-Control: max-age=0
Origin: http://localhost
User-Agent: //省略若干
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBq7AMhcljN14rJrU
?
// 上面的是關鍵
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost/test/test.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.3
?
// 以下為POST提交的內容
?
------WebKitFormBoundaryBq7AMhcljN14rJrU
Content-Disposition: form-data; name="MAX_FILE_SIZE"
?
10240
------WebKitFormBoundaryBq7AMhcljN14rJrU
Content-Disposition: form-data; name="ufile"; filename="logo.png"
Content-Type: image/png //這里就是我們想要的文件類型
?
//以下為文件內容
如果我們在upload_test.php文件中打印$_FILES,可以看到上傳文件類型為image/png。對應上面的請求頭,image/png在文件內容輸出的前面的Content-Type字段中。基本上我們知道了上傳的文件類型是瀏覽器自己識別,直接以文件的Content-Type字段傳遞給服務器。如果有多個文件上傳,就會有多個boundary分隔文件內容,形成多個POST內容塊。那么這些內容在PHP中是如何解析的呢?
當客戶端發起文件提交請求時,Apache會將所接收到的內容轉交給mod_php5模塊。當PHP接收到請求后,首先會調用sapi_activate,在此函數中程序會根據請求的方法處理數據,如示例中POST方法,其調用過程如下:
if(!strcmp(SG(request_info).request_method, "POST")
&& (SG(request_info).content_type)) {
/* HTTP POST -> may contain form data to be read into variables depending on content type given */
sapi_read_post_data(TSRMLS_C);
}
sapi_read_post_data在main/SAPI.c中實現,它會根據POST內容的Content-Type類型來選擇處理POST內容的方法。
if (zend_hash_find(&SG(known_post_content_types), content_type,
content_type_length+1, (void **) &post_entry) == SUCCESS) {
/* found one, register it for use */
SG(request_info).post_entry = post_entry;
post_reader_func = post_entry->post_reader;
}
以上代碼的關鍵在于SG(known_post_content_types)變量,此變更是在SAPI啟動時初始化全局變量時被一起初始化的,其基本過程如下:
sapi_startup
sapi_globals_ctor(&sapi_globals);
php_setup_sapi_content_types(TSRMLS_C);
sapi_register_post_entries(php_post_entries TSRMLS_CC);
這里的的php_post_entries定義在main/php_content_types.c文件。如下:
/* {{{ php_post_entries[] */
static sapi_post_entry php_post_entries[] = {
{ DEFAULT_POST_CONTENT_TYPE, sizeof(DEFAULT_POST_CONTENT_TYPE)-1, sapi_read_standard_form_data, php_std_post_handler },
{ MULTIPART_CONTENT_TYPE, sizeof(MULTIPART_CONTENT_TYPE)-1, NULL, rfc1867_post_handler },
{ NULL, 0, NULL, NULL }
};
/* }}} */
?
#define MULTIPART_CONTENT_TYPE "multipart/form-data"
?
#define DEFAULT_POST_CONTENT_TYPE "application/x-www-form-urlencoded"
如上所示的MULTIPART_CONTENT_TYPE(multipart/form-data)所對應的rfc1867_post_handler方法就是處理$_FILES的核心函數,其定義在main/rfc1867.c文件:SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)后面獲取Content-Type的過程就比較簡單了:
- 通過multipart_buffer_eof控制循環,遍歷所有的multipart部分
- 通過multipart_buffer_headers獲取multipart部分的頭部信息
- 通過php_mime_get_hdr_value(header, “Content-Type”)獲取類型
- 通過register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC);將數據寫到$_FILES變量。
main/rfc1867.c
?
SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler)
{
?
//若干省略
while (!multipart_buffer_eof(mbuff TSRMLS_CC)){
if (!multipart_buffer_headers(mbuff, &header TSRMLS_CC)) {
goto fileupload_done;
}
//若干省略
/* Possible Content-Type: */
if (cancel_upload || !(cd = php_mime_get_hdr_value(header, "Content-Type"))) {
cd = "";
} else {
/* fix for Opera 6.01 */
s = strchr(cd, ';');
if (s != NULL) {
*s = '\0';
}
}
//若干省略
/* Add $foo[type] */
if (is_arr_upload) {
snprintf(lbuf, llen, "%s[type][%s]", abuf, array_index);
} else {
snprintf(lbuf, llen, "%s[type]", param);
}
register_http_post_files_variable(lbuf, cd, http_post_files, 0 TSRMLS_CC);
//若干省略
}
}
其它的$_FILES中的size、name等字段,其實現過程與type類似。
## 預定義變量的獲取
在某個局部函數中使用類似于$GLOBALS變量這樣的預定義變量,如果在此函數中有改變的它們的值的話,這些變量在其它局部函數調用時會發現也會同步變化。為什么呢?是否是這些變量存放在一個集中存儲的地方?從PHP中間代碼的執行來看,這些變量是存儲在一個集中的地方:EG(symbol_table)。
在模塊初始化時,$GLOBALS在zend_startup函數中通過調用zend_register_auto_global將GLOBALS注冊為預定義變量。$_GET、$_POST等在php_startup_auto_globals函數中通過zend_register_auto_global將_GET、_POST等注冊為預定義變量。
在通過$獲取變量時,PHP內核都會通過這些變量名區分是否為全局變量(ZEND_FETCH_GLOBAL),其調用的判斷函數為zend_is_auto_global,這個過程是在生成中間代碼過程中實現的。如果是ZEND_FETCH_GLOBAL或ZEND_FETCH_GLOBAL_LOCK(global語句后的效果),則在獲取獲取變量表時(zend_get_target_symbol_table),直接返回EG(symbol_table)。則這些變量的所有操作都會在全局變量表進行。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊