【PHP7內核剖析】3.3 Zend引擎執行過程

更多《PHP7內核剖析》系列文章:https://github.com/pangudashu/php7-internal

3.3 Zend引擎執行過程

Zend引擎主要包含兩個核心部分:編譯、執行:

zend_vm

前面分析了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引擎執行階段的輸入,整個執行階段的操作都是圍繞着這個結構,關於其具體結構前面我們已經講過了。

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中,比較重要的幾個字段含義如下圖所示:

EG

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的關聯關係:

zend_ex_op

3.3.2 執行流程

Zend的executor與linux二進制程序執行的過程是非常類似的,在C程序執行時有兩個寄存器ebp、esp分別指向當前作用棧的棧頂、棧底,局部變量全部分配在當前棧,函數調用、返回通過callret指令完成,調用時call將當前執行位置壓入棧中,返回時ret將之前執行位置出棧,跳回舊的位置繼續執行,在Zend VM中zend_execute_data就扮演了這兩個角色,zend_execute_data.prev_execute_data保存的是調用方的信息,實現了call/retzend_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_execute

接下來詳細看下整個流程。

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 = 1last_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,而在調用內部函數時則只需要分配實際傳入參數的空間即可,內部函數不會有臨時變量的概念。

最終分配的內存空間如下圖:

var_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腳本最初的那個,並不是指函數調用時的,這一步的操作主要是設置幾個指針:oplinecallreturn_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關係:

zend_exe_init

注意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類型不需要),通過引用計數管理,當在被調函數內部改寫參數的值時將重新拷貝一份,與普通的變量用法相同。

func_exe_send_var

圖中畫的只是上面示例那種情況,比如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; 
}

func_call

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語法實現》一節再具體說明。

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