【PHP7核心剖析】3.3 Zend引擎執行過程
3.3 Zend引擎執行過程
Zend引擎主要包含兩個核心部分:編譯、執行:
前面分析了Zend的編譯過程以及PHP使用者函式的實現,接下來分析下Zend引擎的執行過程。
3.3.1 資料結構
執行流程中有幾個重要的資料結構,先看下這幾個結構。
3.3.1.1 opcode
opcode是將PHP程式碼編譯產生的Zend虛擬機器可識別的指令,php7共有173個opcode,定義在zend_vm_opcodes.h
中,PHP中的所有語法實現都是由這些opcode組成的。
struct _zend_op {
const void *handler; //對應執行的C語言function,即每條opcode都有一個C function處理
znode_op op1; //運算元1
znode_op op2; //運算元2
znode_op result; //返回值
uint32_t extended_value;
uint32_t lineno;
zend_uchar opcode; //opcode指令
zend_uchar op1_type; //運算元1型別
zend_uchar op2_type; //運算元2型別
zend_uchar result_type; //返回值型別
};
3.3.1.2 zend_op_array
zend_op_array
這裡再重複說下zend_op_array幾個核心組成部分:
* opcode指令:即PHP程式碼具體對應的處理動作,與二進位制程式中的程式碼段對應
* 字面量儲存:PHP程式碼中定義的一些變數初始值、呼叫的函式名稱、類名稱、常量名稱等等稱之為字面量,這些值用於執行時初始化變數、函式呼叫等等
* 變數分配情況:與字面量類似,這裡指的是當前opcodes定義了多少變數、臨時變數,每個變數都有一個對應的編號,執行初始化按照總的數目一次性分配zval,使用時也完全按照編號索引,而不是根據變數名索引
3.3.1.3 zend_executor_globals
zend_executor_globals executor_globals
是PHP整個生命週期中最主要的一個結構,是一個全域性變數,在main執行前分配(非ZTS下),直到PHP退出,它記錄著當前請求全部的資訊,經常見到的一個巨集EG
操作的就是這個結構。
//zend_compile.c
#ifndef ZTS
ZEND_API zend_compiler_globals compiler_globals;
ZEND_API zend_executor_globals executor_globals;
#endif
//zend_globals_macros.h
# define EG(v) (executor_globals.v)
zend_executor_globals
結構非常大,定義在zend_globals.h
中,比較重要的幾個欄位含義如下圖所示:
3.3.1.4 zend_execute_data
zend_execute_data
是執行過程中最核心的一個結構,每次函式的呼叫、include/require、eval等都會生成一個新的結構,它表示當前的作用域、程式碼的執行位置以及區域性變數的分配等等,等同於機器碼執行過程中stack的角色,後面分析具體執行流程的時候會詳細分析其作用。
#define EX(element) ((execute_data)->element)
//zend_compile.h
struct _zend_execute_data {
const zend_op *opline; //指向當前執行的opcode,初始時指向zend_op_array起始位置
zend_execute_data *call; /* current call */
zval *return_value; //返回值指標 */
zend_function *func; //當前執行的函式(非函式呼叫時為空)
zval This; //這個值並不僅僅是面向物件的this,還有另外兩個值也通過這個記錄:call_info + num_args,分別存在zval.u1.reserved、zval.u2.num_args
zend_class_entry *called_scope; //當前call的類
zend_execute_data *prev_execute_data; //函式呼叫時指向呼叫位置作用空間
zend_array *symbol_table; //全域性變數符號表
#if ZEND_EX_USE_RUN_TIME_CACHE
void **run_time_cache; /* cache op_array->run_time_cache */
#endif
#if ZEND_EX_USE_LITERALS
zval *literals; //字面量陣列,與func.op_array->literals相同
#endif
};
zend_execute_data與zend_op_array的關聯關係:
3.3.2 執行流程
Zend的executor與linux二進位制程式執行的過程是非常類似的,在C程式執行時有兩個暫存器ebp、esp分別指向當前作用棧的棧頂、棧底,區域性變數全部分配在當前棧,函式呼叫、返回通過call
、ret
指令完成,呼叫時call
將當前執行位置壓入棧中,返回時ret
將之前執行位置出棧,跳回舊的位置繼續執行,在Zend VM中zend_execute_data
就扮演了這兩個角色,zend_execute_data.prev_execute_data
儲存的是呼叫方的資訊,實現了call/ret
,zend_execute_data
後面會分配額外的記憶體空間用於區域性變數的儲存,實現了ebp/esp
的作用。
注意:在執行前分配記憶體時並不僅僅是分配了zend_execute_data
大小的空間,除了sizeof(zend_execute_data)
外還會額外申請一塊空間,用於分配區域性變數、臨時(中間)變數等,具體的分配過程下面會講到。
Zend執行opcode的簡略過程:
* step1: 為當前作用域分配一塊記憶體,充當執行棧,zend_execute_data結構、所有區域性變數、中間變數等等都在此記憶體上分配
* step2: 初始化全域性變數符號表,然後將全域性執行位置指標EG(current_execute_data)指向step1新分配的zend_execute_data,然後將zend_execute_data.opline指向op_array的起始位置
* step3: 從EX(opline)開始呼叫各opcode的C處理handler(即_zend_op.handler),每執行完一條opcode將EX(opline)++
繼續執行下一條,直到執行完全部opcode,函式/類成員方法呼叫、if的執行過程:
* step3.1: if語句將根據條件的成立與否決定EX(opline) + offset
所加的偏移量,實現跳轉
* step3.2: 如果是函式呼叫,則首先從EG(function_table)中根據function_name取出此function對應的編譯完成的zend_op_array,然後像step1一樣新分配一個zend_execute_data結構,將EG(current_execute_data)賦值給新結構的prev_execute_data
,再將EG(current_execute_data)指向新的zend_execute_data,最後從新的zend_execute_data.opline
開始執行,切換到函式內部,函式執行完以後將EG(current_execute_data)重新指向EX(prev_execute_data),釋放分配的執行棧,銷燬區域性變數,繼續從原來函式呼叫的位置執行
* step3.3: 類方法的呼叫與函式基本相同,後面分析物件實現的時候再詳細分析
* step4: 全部opcode執行完成後將step1分配的記憶體釋放,這個過程會將所有的區域性變數”銷燬”,執行階段結束
接下來詳細看下整個流程。
Zend執行入口為位於zend_vm_execute.h
檔案中的zend_execute():
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
if (EG(exception) != NULL) {
return;
}
//分配zend_execute_data
execute_data = zend_vm_stack_push_call_frame(ZEND_CALL_TOP_CODE,
(zend_function*)op_array, 0, zend_get_called_scope(EG(current_execute_data)), zend_get_this_object(EG(current_execute_data)));
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data); //=> execute_data->prev_execute_data = EG(current_execute_data);
i_init_execute_data(execute_data, op_array, return_value); //初始化execute_data
zend_execute_ex(execute_data); //執行opcode
zend_vm_stack_free_call_frame(execute_data); //釋放execute_data:銷燬所有的PHP變數
}
上面的過程分為四步:
(1)分配stack
由zend_vm_stack_push_call_frame
函式分配一塊用於當前作用域的記憶體空間,返回結果是zend_execute_data
的起始位置。
//zend_execute.h
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame(uint32_t call_info, zend_function *func, uint32_t num_args, ...)
{
uint32_t used_stack = zend_vm_calc_used_stack(num_args, func);
return zend_vm_stack_push_call_frame_ex(used_stack, call_info,
func, num_args, called_scope, object);
}
首先根據zend_execute_data
、當前zend_op_array
中區域性/臨時變數數計算需要的記憶體空間:
//zend_execute.h
static zend_always_inline uint32_t zend_vm_calc_used_stack(uint32_t num_args, zend_function *func)
{
uint32_t used_stack = ZEND_CALL_FRAME_SLOT + num_args; //內部函式只用這麼多,臨時變數是編譯過程中根據PHP的程式碼優化出的值,比如:`"hi~".time()`,而在內部函式中則沒有這種情況
if (EXPECTED(ZEND_USER_CODE(func->type))) { //在php指令碼中寫的function
used_stack += func->op_array.last_var + func->op_array.T - MIN(func->op_array.num_args, num_args);
}
return used_stack * sizeof(zval);
}
//zend_compile.h
#define ZEND_CALL_FRAME_SLOT \
((int)((ZEND_MM_ALIGNED_SIZE(sizeof(zend_execute_data)) + ZEND_MM_ALIGNED_SIZE(sizeof(zval)) - 1) / ZEND_MM_ALIGNED_SIZE(sizeof(zval))))
回想下前面編譯階段zend_op_array的結果,在編譯過程中已經確定當前作用域下有多少個區域性變數(func->op_array.last_var)、臨時/中間/無用變數(func->op_array.T),從而在執行之初就將他們全部分配完成:
- last_var:PHP程式碼中定義的變數數,zend_op.op{1|2}_type = IS_CV 或 result_type & IS_CV的全部數量
- T:表示用到的臨時變數、無用變數等,zend_op.op{1|2}_type = IS_TMP_VAR|IS_VAR 或resulte_type & (IS_TMP_VAR|IS_VAR)的全部數量
比如賦值操作:$a = 1234;
,編譯後last_var = 1,T = 1
,last_var
有$a
,這裡為什麼會有T
?因為賦值語句有一個結果返回值,只是這個值沒有用到,假如這麼用結果就會用到了if(($a = 1234) == true){...}
,這時候$a = 1234;
的返回結果型別是IS_VAR
,記在T
上。
num_args
為函式呼叫時的實際傳入引數數量,func->op_array.num_args
為全部引數數量,所以MIN(func->op_array.num_args, num_args)
等於num_args
,在自定義函式中used_stack=ZEND_CALL_FRAME_SLOT + func->op_array.last_var + func->op_array.T
,而在呼叫內部函式時則只需要分配實際傳入引數的空間即可,內部函式不會有臨時變數的概念。
最終分配的記憶體空間如下圖:
這裡實際分配記憶體時並不是直接malloc
的,還記得上面EG結構中有個vm_stack
嗎?實際記憶體是從這裡獲取的,每次從EG(vm_stack_top)
處開始分配,分配完再將此指標指向EG(vm_stack_top) + used_stack
,這裡不再對vm_stack作更多分析,更下層實際就是Zend的記憶體池(zend_alloc.c),後面也會單獨分析。
static zend_always_inline zend_execute_data *zend_vm_stack_push_call_frame_ex(uint32_t used_stack, ...)
{
zend_execute_data *call = (zend_execute_data*)EG(vm_stack_top);
...
//當前vm_stack是否夠用
if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) {
call = (zend_execute_data*)zend_vm_stack_extend(used_stack); //新開闢一塊vm_stack
...
}else{ //空間夠用,直接分配
EG(vm_stack_top) = (zval*)((char*)call + used_stack);
...
}
call->func = func;
...
return call;
}
(2)初始化zend_execute_data
注意,這裡的初始化是整個php指令碼最初的那個,並不是指函式呼叫時的,這一步的操作主要是設定幾個指標:opline
、call
、return_value
,同時將PHP的全域性變數新增到EG(symbol_table)
中去:
//zend_execute.c
static zend_always_inline void i_init_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value)
{
EX(opline) = op_array->opcodes;
EX(call) = NULL;
EX(return_value) = return_value;
if (UNEXPECTED(EX(symbol_table) != NULL)) {
...
zend_attach_symbol_table(execute_data);//將全域性變數新增到EG(symbol_table)中一份,因為此處的execute_data是PHP指令碼最初的那個,不是function的,所以所有的變數都是全域性的
}else{ //這個分支的情況還未深入分析,後面碰到再補充
...
}
}
(3)執行opcode
這一步開始具體執行opcode指令,這裡呼叫的是zend_execute_ex
,這是一個函式指標,如果此指標沒有被任何擴充套件重新定義那麼將由預設的execute_ex
處理:
# define ZEND_OPCODE_HANDLER_ARGS_PASSTHRU execute_data
ZEND_API void execute_ex(zend_execute_data *ex)
{
zend_execute_data *execute_data = ex;
while(1) {
int ret;
if (UNEXPECTED((ret = ((opcode_handler_t)EX(opline)->handler)(execute_data /*ZEND_OPCODE_HANDLER_ARGS_PASSTHRU*/)) != 0)) {
if (EXPECTED(ret > 0)) { //調到新的位置執行:函式呼叫時的情況
execute_data = EG(current_execute_data);
}else{
return;
}
}
}
}
大概的執行過程上面已經介紹過了,這裡只分析下整體執行流程,至於PHP各語法具體的handler處理後面會單獨列一章詳細分析。
(4)釋放stack
這一步就比較簡單了,只是將申請的zend_execute_data
記憶體釋放給記憶體池(注意這裡並不是變數的銷燬),具體的操作只需要修改幾個指標即可:
static zend_always_inline void zend_vm_stack_free_call_frame_ex(uint32_t call_info, zend_execute_data *call)
{
ZEND_ASSERT_VM_STACK_GLOBAL;
if (UNEXPECTED(call_info & ZEND_CALL_ALLOCATED)) {
zend_vm_stack p = EG(vm_stack);
zend_vm_stack prev = p->prev;
EG(vm_stack_top) = prev->top;
EG(vm_stack_end) = prev->end;
EG(vm_stack) = prev;
efree(p);
} else {
EG(vm_stack_top) = (zval*)call;
}
ZEND_ASSERT_VM_STACK_GLOBAL;
}
static zend_always_inline void zend_vm_stack_free_call_frame(zend_execute_data *call)
{
zend_vm_stack_free_call_frame_ex(ZEND_CALL_INFO(call), call);
}
3.3.3 函式的執行流程
(這裡的函式指使用者自定義的PHP函式,不含內部函式)
上一節我們介紹了zend執行引擎的幾個關鍵步驟,也簡單的介紹了函式的呼叫過程,這裡再單獨總結下:
- 【初始化階段】:這個階段首先查詢到函式的zend_function,普通function就是到EG(function_table)中查詢,成員方法則先從EG(class_table)中找到zend_class_entry,然後再進一步在其function_table找到zend_function,接著就是根據zend_op_array新分配zend_execute_data結構並設定上下文切換的指標
- 【引數傳遞階段】:如果函式沒有引數則跳過此步驟,有的話則會將函式所需引數傳遞到初始化階段新分配的zend_execute_data動態變數區
- 【函式呼叫階段】:這個步驟主要是做上下文切換,將執行器切換到呼叫的函式上,可以理解會在這個階段遞迴呼叫zend_execute_ex函式實現call的過程(實際並一定是遞迴,預設是在while(1){…}中切換執行空間的,但如果我們在擴充套件中重定義了zend_execute_ex用來介入執行流程則就是遞迴呼叫)
- 【函式執行階段】:被呼叫函式內部的執行過程,首先是接收引數,然後開始執行opcode
- 【函式返回階段】:被呼叫函式執行完畢返回過程,將返回值傳遞給呼叫方的zend_execute_data變數區,然後釋放zend_execute_data以及分配的區域性變數,將上下文切換到呼叫前,回到呼叫的位置繼續執行,這個實際是函式執行中的一部分,不算是獨立的一個過程
接下來我們一個具體的例子詳細分析下各個階段的處理過程:
function my_function($a, $b = false, $c = "hi"){
return $c;
}
$a = array();
$b = true;
my_function($a, $b);
主指令碼、my_function的opcode為:
3.3.3.1 初始化階段
此階段的主要工作有兩個:查詢函式zend_function、分配zend_execute_data。
上面的例子此過程執行的opcode為ZEND_INIT_FCALL
,根據op_type計算可得handler為ZEND_INIT_FCALL_SPEC_CONST_HANDLER
:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *fname = EX_CONSTANT(opline->op2); //呼叫的函式名稱通過運算元2記錄
zval *func;
zend_function *fbc;
zend_execute_data *call;
//這裡牽扯到zend的一種快取機制:執行時快取,後面我們會單獨分析,這裡忽略即可
...
//首先根據函式名去EG(function_table)索引zend_function
func = zend_hash_find(EG(function_table), Z_STR_P(fname));
if (UNEXPECTED(func == NULL)) {
SAVE_OPLINE();
zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname));
HANDLE_EXCEPTION();
}
fbc = Z_FUNC_P(func); //(*func).value.func
...
//分配zend_execute_data
call = zend_vm_stack_push_call_frame_ex(
opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
fbc, opline->extended_value, NULL, NULL);
call->prev_execute_data = EX(call);
EX(call) = call; //將當前正在執行的zend_execute_data.call指向新分配的zend_execute_data
ZEND_VM_NEXT_OPCODE();
}
當前zend_execute_data及新生成的zend_execute_data關係:
注意This這個值,它並不僅僅指的是面向物件中那個this,此外它還記錄著其它兩個資訊:
* call_info:呼叫資訊,通過This.u1.reserved記錄,因為我們的主指令碼、使用者自定義函式呼叫、核心函式呼叫、include/require/eval等都會生成一個zend_execute_data,這個值就是用來區分這些不同型別的,對應的具體值為:ZEND_CALL_TOP_CODE、ZEND_CALL_NESTED_FUNCTION、ZEND_CALL_TOP_FUNCTION、ZEND_CALL_NESTED_CODE,這個資訊是在分配zend_execute_data時顯式宣告的
* num_args:函式呼叫實際傳入的引數數量,通過This.u2.num_args記錄,比如示例中我們定義的函式有3個引數,其中1個是必傳的,而我們呼叫時傳入了2個,所以這個例子中的num_args就是2,這個值在編譯時知道的,儲存在zend_op->extended_value中
3.3.3.2 引數傳遞階段
這個過程就是將當前作用空間下的變數值”複製”到新的zend_execute_data動態變數區中,那麼呼叫方怎麼知道要把值傳遞到新zend_execute_data哪個位置呢?實際這個地方是有固定規則的,zend_execute_data的動態變數區最前面是引數變數,按照引數的順序依次分配,接著才是普通的區域性變數、臨時變數等,所以呼叫方就可以根據傳的是第幾個引數來確定其具體的儲存位置。
另外這裡的”複製”並不是硬拷貝,而是傳遞的value指標(當然bool/int/double型別不需要),通過引用計數管理,當在被調函式內部改寫引數的值時將重新拷貝一份,與普通的變數用法相同。
圖中畫的只是上面示例那種情況,比如my_function(array());
直接傳值則會是literals區->新zend_execute_data動態變數區的傳遞。
3.3.3.3 函式呼叫階段
這個過程主要是進行一些上下文切換,將執行器切換到呼叫的函式上。
上面例子對應的opcode為ZEND_DO_UCALL
,handler為ZEND_DO_UCALL_SPEC_HANDLER
:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_DO_UCALL_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zend_execute_data *call = EX(call);
zend_function *fbc = call->func;
zval *ret;
SAVE_OPLINE();
EX(call) = call->prev_execute_data;
EG(scope) = NULL;
ret = NULL;
call->symbol_table = NULL;
if (RETURN_VALUE_USED(opline)) {
ret = EX_VAR(opline->result.var); //函式返回值的儲存位置
ZVAL_NULL(ret);
Z_VAR_FLAGS_P(ret) = 0;
}
call->prev_execute_data = execute_data; //將新zend_execute_data->prev_execute_data指向當前data
i_init_func_execute_data(call, &fbc->op_array, ret, 0);
ZEND_VM_ENTER();
}
//zend_execute.c
static zend_always_inline void i_init_func_execute_data(zend_execute_data *execute_data, zend_op_array *op_array, zval *return_value, int check_this)
{
uint32_t first_extra_arg, num_args;
ZEND_ASSERT(EX(func) == (zend_function*)op_array);
EX(opline) = op_array->opcodes;
EX(call) = NULL;
EX(return_value) = return_value;
first_extra_arg = op_array->num_args; //函式的總引數數量,示例中為3
num_args = EX_NUM_ARGS(); //實際傳入引數數量,示例中為2
if (UNEXPECTED(num_args > first_extra_arg)) {
...
} else if (EXPECTED((op_array->fn_flags & ZEND_ACC_HAS_TYPE_HINTS) == 0)) {
//跳過前面幾個已經傳參的引數接收的指令,因為已經顯式的傳遞引數了,無需再接收預設值
EX(opline) += num_args;
}
//初始化動態變數區,將所有變數(除已經傳入的外)設定為IS_UNDEF
if (EXPECTED((int)num_args < op_array->last_var)) {
zval *var = EX_VAR_NUM(num_args);
zval *end = EX_VAR_NUM(op_array->last_var);
do {
ZVAL_UNDEF(var);
var++;
} while (var != end);
}
...
//分配執行時快取,此機制後面再單獨說明
if (UNEXPECTED(!op_array->run_time_cache)) {
op_array->run_time_cache = zend_arena_alloc(&CG(arena), op_array->cache_size);
memset(op_array->run_time_cache, 0, op_array->cache_size);
}
EX_LOAD_RUN_TIME_CACHE(op_array); //execute_data.run_time_cache = op_array.run_time_cache
EX_LOAD_LITERALS(op_array); //execute_data.literals = op_array.literals
//EG(current_execute_data)為執行器當前執行空間,將執行器切到函式內
EG(current_execute_data) = execute_data;
}
3.3.3.4 函式執行階段
這個過程就是函式內部opcode的執行流程,沒什麼特別的,唯一的不同就是前面會接收未傳的引數,如下圖所示。
3.3.3.5 函式返回階段
實際此過程可以認為是3.3.3.4的一部分,這個階段就是函式呼叫結束,返回呼叫處的過程,這個過程中有三個關鍵工作:拷貝返回值、執行器切回撥用位置、釋放清理區域性變數。
上面例子此過程opcode為ZEND_RETURN
,對應的handler為ZEND_RETURN_SPEC_CV_HANDLER
:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RETURN_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *retval_ptr;
zend_free_op free_op1;
//獲取返回值
retval_ptr = _get_zval_ptr_cv_undef(execute_data, opline->op1.var);
if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(retval_ptr) == IS_UNDEF)) {
//返回值未定義,返回NULL
retval_ptr = GET_OP1_UNDEF_CV(retval_ptr, BP_VAR_R);
if (EX(return_value)) {
ZVAL_NULL(EX(return_value));
}
} else if(!EX(return_value)){
...
}else{ //返回值正常
...
ZVAL_DEREF(retval_ptr); //如果retval_ptr是引用則將找到其具體引用的zval
ZVAL_COPY(EX(return_value), retval_ptr); //將返回值複製給呼叫方接收值:EX(return_value)
...
}
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
繼續看下zend_leave_helper_SPEC
,執行器切換、區域性變數清理就是在這個函式中完成的。
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
zend_execute_data *old_execute_data;
uint32_t call_info = EX_CALL_INFO();
if (EXPECTED(ZEND_CALL_KIND_EX(call_info) == ZEND_CALL_NESTED_FUNCTION)) {
//普通的函式呼叫將走到這個分支
i_free_compiled_variables(execute_data);
...
}
//include、eval及整個指令碼的結束(main函式)走到下面
//...
//將執行器切回撥用的位置
EG(current_execute_data) = EX(prev_execute_data);
}
//zend_execute.c
//清理區域性變數的過程
static zend_always_inline void i_free_compiled_variables(zend_execute_data *execute_data)
{
zval *cv = EX_VAR_NUM(0);
zval *end = cv + EX(func)->op_array.last_var;
while (EXPECTED(cv != end)) {
if (Z_REFCOUNTED_P(cv)) {
if (!Z_DELREF_P(cv)) { //引用計數減一後為0
zend_refcounted *r = Z_COUNTED_P(cv);
ZVAL_NULL(cv);
zval_dtor_func_for_ptr(r); //釋放變數值
} else {
GC_ZVAL_CHECK_POSSIBLE_ROOT(cv); //引用計數減一後>0,啟動垃圾檢查機制,清理迴圈引用導致無法回收的垃圾
}
}
cv++;
}
}
除了函式呼叫完成時有return操作,其它還有兩種情況也會有此過程:
* 1.PHP主指令碼執行結束時:也就是PHP指令碼開始執行的入口指令碼(PHP沒有顯式的main函式,這種就可以認為是main函式),但是這種情況並不會在return時清理,因為在main函式中定義的變數並非純碎的局面變數,它們都是全域性變數,與__GET、__POST是一類,這些全域性變數的清理是在request_shutdown階段處理
* 2.include、eval:以include為例,如果include的檔案中定義了全域性變數,那麼這些變數實際與上面1的情況一樣,它們的儲存位置是在一起的
所以實際上面說的這兩種情況屬於一類,它們並不是區域性變數的清理,而是全域性變數的清理,另外區域性變數的清理也並非只有return一個時機,另外還有一個更重要的時機就是變數分離時,這種情況我們在《PHP語法實現》一節再具體說明。