在[<< 第二章第三小節 PHP腳本的執行 -- opcode >>](#)中, 我們對opcode進行了一個簡略的說明。這一小節我們講這些中間代碼在Zend虛擬機中是如何被執行的。
假如我們現在使用的是CLI模式,直接在SAPI/cli/php_cli.c文件中找到main函數,默認情況下PHP的CLI模式的行為模式為PHP_MODE_STANDARD。此行為模式中PHP內核會調用php_execute_script(&file_handle TSRMLS_CC);來執行PHP文件。順著這條執行的線路,可以看到一個PHP文件在經過詞法分析,語法分析,編譯后生成中間代碼的過程:
EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC);
在銷毀了文件所在的handler后,如果存在中間代碼,則PHP虛擬機將通過以下代碼執行中間代碼:
zend_execute(EG(active_op_array) TSRMLS_CC);
如果你是使用VS查看源碼的話,將光標移到zend_execute并直接按F12,你會發現zend_execute的定義跳轉到了一個指針函數的聲明(Zend/zend_execute_API.c)。
ZEND_API void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);
這是一個全局的函數指針,它的作用就是執行PHP代碼文件解析完的轉成的zend_op_array。和zend_execute相同的還有一個zedn_execute_internal函數,它用來執行內部函數。在PHP內核啟動時(zend_startup)時,這個全局函數指針將會指向execute函數。注意函數指針前面的修飾符ZEND_API,這是ZendAPI的一部分。在zend_execute函數指針賦值時,還有PHP的中間代碼編譯函數zend_compile_file(文件形式)和zend_compile_string(字符串形式)。
zend_compile_file = compile_file;
zend_compile_string = compile_string;
zend_execute = execute;
zend_execute_internal = NULL;
zend_throw_exception_hook = NULL;
這幾個全局的函數指針均只調用了系統默認實現的幾個函數,比如compile_file和compile_string函數,他們都是以全局函數指針存在,這種實現方式在PHP內核中比比皆是,其優勢在于更低的耦合度,甚至可以定制這些函數。比如在APC等opcode優化擴展中就是通過替換系統默認的zend_compile_file函數指針為自己的函數指針my_compile_file,并且在my_compile_file中增加緩存等功能。
到這里我們找到了中間代碼執行的最終函數:execute(Zend/zend_vm_execure.h)。在這個函數中所有的中間代碼的執行最終都會調用handler。這個handler是什么呢?
if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
}
這里的handler是一個函數指針,它指向執行該opcode時調用的處理函數。此時我們需要看看handler函數指針是如何被設置的。在前面我們有提到和execute一起設置的全局指針函數:zend_compile_string。它的作用是編譯字符串為中間代碼。在Zend/zend_language_scanner.c文件中有compile_string函數的實現。在此函數中,當解析完中間代碼后,一般情況下,它會執行pass_two(Zend/zend_opcode.c)函數。pass_two這個函數,從其命名上真有點看不出其意義是什么。但是我們關注的是在函數內部,它遍歷整個中間代碼集合,調用ZEND_VM_SET_OPCODE_HANDLER(opline);為每個中間代碼設置處理函數。ZEND_VM_SET_OPCODE_HANDLER是zend_vm_set_opcode_handler函數的接口宏,zend_vm_set_opcode_handler函數定義在Zend/zend_vm_execute.h文件。其代碼如下:
static opcode_handler_t zend_vm_get_opcode_handler(zend_uchar opcode, zend_op* op)
{
static const int zend_vm_decode[] = {
_UNUSED_CODE, /* 0 */
_CONST_CODE, /* 1 = IS_CONST */
_TMP_CODE, /* 2 = IS_TMP_VAR */
_UNUSED_CODE, /* 3 */
_VAR_CODE, /* 4 = IS_VAR */
_UNUSED_CODE, /* 5 */
_UNUSED_CODE, /* 6 */
_UNUSED_CODE, /* 7 */
_UNUSED_CODE, /* 8 = IS_UNUSED */
_UNUSED_CODE, /* 9 */
_UNUSED_CODE, /* 10 */
_UNUSED_CODE, /* 11 */
_UNUSED_CODE, /* 12 */
_UNUSED_CODE, /* 13 */
_UNUSED_CODE, /* 14 */
_UNUSED_CODE, /* 15 */
_CV_CODE /* 16 = IS_CV */
};
return zend_opcode_handlers[opcode * 25
+ zend_vm_decode[op->op1.op_type] * 5
+ zend_vm_decode[op->op2.op_type]];
}
?
ZEND_API void zend_vm_set_opcode_handler(zend_op* op)
{
op->handler = zend_vm_get_opcode_handler(zend_user_opcodes[op->opcode], op);
}
在前面章節[<< 第二章第三小節 -- opcode處理函數查找 >>](#)中介紹了四種查找opcode處理函數的方法,而根據其本質實現查找也在其中,只是這種方法對于計算機來說比較容易識別,而對于自然人來說卻不太友好。比如一個簡單的A + B的加法運算,如果你想用這種方法查找其中間代碼的實現位置的話,首先你需要知道中間代碼的代表的值,然后知道第一個表達式和第二個表達式結果的類型所代表的值,然后計算得到一個數值的結果,然后從數組zend_opcode_handlers找這個位置,位置所在的函數就是中間代碼的函數。這對閱讀代碼的速度沒有好處,但是在開始閱讀代碼的時候根據代碼的邏輯走這樣一個流程卻是大有好處。
回到正題。handler所指向的方法基本都存在于Zend/zend_vm_execute.h文件文件。知道了handler的由來,我們就知道每個opcode調用handler指針函數時最終調用的位置。
在opcode的處理函數執行完它的本職工作后,常規的opcode都會在函數的最后面添加一句:ZEND_VM_NEXT_OPCODE();。這是一個宏,它的作用是將當前的opcode指針指向下一條opcode,并且返回0。如下代碼:
#define ZEND_VM_NEXT_OPCODE() \
CHECK_SYMBOL_TABLES() \
EX(opline)++; \
ZEND_VM_CONTINUE()
?
#define ZEND_VM_CONTINUE() return 0
在execute函數中,處理函數的執行是在一個while(1)循環作用范圍中。如下:
?
while (1) {
int ret;
#ifdef ZEND_WIN32
if (EG(timed_out)) {
zend_timeout(0);
}
#endif
?
if ((ret = EX(opline)->handler(execute_data TSRMLS_CC)) > 0) {
switch (ret) {
case 1:
EG(in_execution) = original_in_execution;
return;
case 2:
op_array = EG(active_op_array);
goto zend_vm_enter;
case 3:
execute_data = EG(current_execute_data);
default:
break;
}
}
?
}
前面說到每個中間代碼在執行完后都會將中間代碼的指針指向下一條指令,并且返回0。當返回0時,while 循環中的if語句都不滿足條件,從而使得中間代碼可以繼續執行下去。正是這個while(1)的循環使得PHP內核中的opcode可以從第一條執行到最后一條,當然這中間也有一些函數的跳轉或類方法的執行等。
以上是一條中間代碼的執行,那么對于函數的遞歸調用,PHP內核是如何處理的呢?看如下一段PHP代碼:
function t($c) {
[echo](http://www.php.net/echo) $c, "\n";
if ($c > 2) {
return ;
}
t($c + 1);
}
t(1);
這是一個簡單的遞歸調用函數實現,它遞歸調用了兩次,這個遞歸調用是如何進行的呢?我們知道函數的調用所在的中間代碼最終是調用zend_do_fcall_common_helper_SPEC(Zend/zend_vm_execute.h)。在此函數中有如下一段:
if (zend_execute == execute && !EG(exception)) {
EX(call_opline) = opline;
ZEND_VM_ENTER();
} else {
zend_execute(EG(active_op_array) TSRMLS_CC);
}
前面提到zend_execute API可能會被覆蓋,這里就進行了簡單的判斷,如果擴展覆蓋了opcode執行函數,則進行特殊的邏輯處理。
上一段代碼中的ZEND_VM_ENTER()定義在Zend/zend_vm_execute.h的開頭,如下:
#define ZEND_VM_CONTINUE() return 0
#define ZEND_VM_RETURN() return 1
#define ZEND_VM_ENTER() return 2
#define ZEND_VM_LEAVE() return 3
這些在中間代碼的執行函數中都有用到,這里的ZEND_VM_ENTER()表示return 2。在前面的內容中我們有說到在調用了EX(opline)->handler(execute_data TSRMLS_CC))后會將返回值賦值給ret。然后根據ret判斷下一步操作,這里的遞歸函數是返回2,于是下一步操作是:
op_array = EG(active_op_array);
goto zend_vm_enter;
這里將EG(active_op_array)的值賦給op_array后,直接跳轉到execute函數的定義的zend_vm_enter標簽,此時的EG(active_op_array)的值已經在zend_do_fcall_common_helper_SPEC中被換成了當前函數的中間代碼集合,其實現代碼為:
if (EX(function_state).function->type == ZEND_USER_FUNCTION) { // 用戶自定義的函數
EX(original_return_value) = EG(return_value_ptr_ptr);
EG(active_symbol_table) = NULL;
EG(active_op_array) = &EX(function_state).function->op_array; // 將當前活動的中間代碼指針指向用戶自定義函數的中間代碼數組
EG(return_value_ptr_ptr) = NULL;
當內核執行完用戶自定義的函數后,怎么返回之前的中間代碼代碼主干路徑呢?這是由于在execute函數中初始化數據時已經將當前的路徑記錄在EX(op_array)中了(EX(op_array) = op_array;)當用戶函數返回時程序會將之前保存的路徑重新恢復到EG(active_op_array)中(EG(active_op_array) = EX(op_array);)。可能此時你會問如果函數沒有返回呢?這種情況在用戶自定義的函數中不會發生的,就算是你沒有寫return語句,PHP內核也會自動給加上一個return語句,這在第四章 [<< 第四章 函數的實現 第二節 函數的定義,傳參及返回值 函數的返回值 >>](#)已經有說明過。
整個調用路徑如下圖所示:

圖7.2 Zend中間代碼調用路徑圖
以上是opcode的執行過程,與過程相比,過程中的數據會更加重要,那么在執行過程中的核心數據結構有哪些呢?在Zend/zend_vm_execute.h文件中的execute函數實現中,zend_execute_data類型的execute_data變量貫穿整個中間代碼的執行過程,其在調用時并沒有直接使用execute_data,而是使用EX宏代替,其定義在Zend/zend_compile.h文件中,如下:
#define EX(element) execute_data.element
因此我們在execute函數或在opcode的實現函數中會看到EX(fbc),EX(object)等宏調用,它們是調用函數局部變量execute_data的元素:execute_data.fbc和execute_data.object。execute_data不僅僅只有fbc、object等元素,它包含了執行過程中的中間代碼,上一次執行的函數,函數執行的當前作用域,類等信息。其結構如下:
typedef struct _zend_execute_data zend_execute_data;
?
struct _zend_execute_data {
struct _zend_op *opline;
zend_function_state function_state;
zend_function *fbc; /* Function Being Called */
zend_class_entry *called_scope;
zend_op_array *op_array; /* 當前執行的中間代碼 */
zval *object;
union _temp_variable *Ts;
zval ***CVs;
HashTable *symbol_table; /* 符號表 */
struct _zend_execute_data *prev_execute_data; /* 前一條中間代碼執行的環境*/
zval *old_error_reporting;
zend_bool nested;
zval **original_return_value; /* */
zend_class_entry *current_scope;
zend_class_entry *current_called_scope;
zval *current_this;
zval *current_object;
struct _zend_op *call_opline;
};
在前面的中間代碼執行過程中有介紹:中間代碼的執行最終是通過EX(opline)->handler(execute_data TSRMLS_CC)來調用最終的中間代碼程序。在這里會將主管中間代碼執行的execute函數中初始化好的execture_data傳遞給執行程序。
zend_execute_data結構體部分字段說明如下:
- opline字段:struct _zend_op類型,當前執行的中間代碼
- op_array字段: zend_op_array類型,當前執行的中間代碼隊列
- fbc字段:zend_function類型,已調用的函數
- called_scope字段:zend_class_entry類型,當前調用對象作用域,常用操作是EX(called_scope) = Z_OBJCE_P(EX(object)),即將剛剛調用的對象賦值給它。
- symbol_table字段: 符號表,存放局部變量,這在前面的[<< 第六節 變量的生命周期 變量的作用域 >>](#)有過說明。在execute_data初始時,EX(symbol_table) = EG(active_symbol_table);
- prev_execute_data字段:前一條中間代碼執行的中間數據,用于函數調用等操作的運行環境恢復。
在execute函數中初始化時,會調用zend_vm_stack_alloc函數分配內存。這是一個棧的分配操作,對于一段PHP代碼的上下文環境,它存在于這樣一個分配的空間作放置中間數據用,并作為棧頂元素。當有其它上下文環境的切換(如函數調用),此時會有一個新的元素生成,上一個上下文環境會被新的元素壓下去,新的上下文環境所在的元素作為棧頂元素存在。
在zend_vm_stack_alloc函數中我們可以看到一些PHP內核中的優化。比如在分配時,這里會存在一個最小分配單元,在zend_vm_stack_extend函數中,分配的最小單位是ZEND_VM_STACK_PAGE_SIZE((64 * 1024) - 64),這樣可以在一定范圍內控制內存碎片的大小。又比如判斷棧元素是否為空,在PHP5.3.1之前版本(如5.3.0)是通過第四個元素elelments與top的位置比較來實現,而從PHP5.3.1版本開始,struct _zend_vm_stack結構就沒有第四個元素,直接通過在當前地址上增加整個結構體的長度與top的地址比較實現。兩個版本結構代碼及比較代碼如下:
// PHP5.3.0
struct _zend_vm_stack {
void **top;
void **end;
zend_vm_stack prev;
void *elements[1];
};
?
if (UNEXPECTED(EG(argument_stack)->top == EG(argument_stack)->elements)) {
}
?
// PHP5.3.1
struct _zend_vm_stack {
void **top;
void **end;
zend_vm_stack prev;
};
?
if (UNEXPECTED(EG(argument_stack)->top == ZEND_VM_STACK_ELEMETS(EG(argument_stack)))) {
}
?
#define ZEND_VM_STACK_ELEMETS(stack) \
((void**)(((char*)(stack)) + ZEND_MM_ALIGNED_SIZE(sizeof(struct _zend_vm_stack))))
當一個上下文環境結束其生命周期后,如果回收這段內存呢?還是以函數為例,我們在前面的函數章節中<< [函數的返回](#) >>中我們知道每個函數都會有一個函數返回,即使沒有在函數的實現中定義,也會默認返回一個NULL。以ZEND_RETURN_SPEC_CONST_HANDLER實現為例,在函數的返回最后都會調用一個函數**zend_leave_helper_SPEC**。
在zend_leave_helper_SPEC函數中,對于執行過程中的函數處理有幾個關鍵點:
- 上下文環境的切換:這里的關鍵代碼是:EG(current_execute_data) = EX(prev_execute_data);。EX(prev_execute_data)用于保留當前函數調用前的上下文環境,從而達到恢復和切換的目的。
- 當前上下文環境所占用內存空間的釋放:這里的關鍵代碼是:zend_vm_stack_free(execute_data TSRMLS_CC);。zend_vm_stack_free函數的實現存在于Zend/zend_execute.h文件,它的作用就是釋放棧元素所占用的內存。
- 返回到之前的中間代碼執行路徑中:這里的關鍵代碼是:ZEND_VM_LEAVE();。我們從zend_vm_execute.h文件的開始部分就知道ZEND_VM_LEAVE宏的效果是返回3。在執行中間代碼的while循環當中,當ret=3時,這個執行過程就會恢復之前上下文環境,繼續執行。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和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中文手冊