### 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以及分配的局部變量,將上下文切換到調用前,回到調用的位置繼續執行,這個實際是函數執行中的一部分,不算是獨立的一個過程
接下來我們一個具體的例子詳細分析下各個階段的處理過程:
```php
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`:
```c
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`:
```c
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`:
```c
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`,執行器切換、局部變量清理就是在這個函數中完成的。
```c
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語法實現》一節再具體說明。
- 前言
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.3.1 概述
- 1.3.2 基本實現
- 1.3.3 FPM的初始化
- 1.3.4 請求處理
- 1.3.5 進程管理
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 第3章 Zend虛擬機
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 3.2 函數實現
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.3.2 執行流程
- 3.3.3 函數的執行流程
- 3.3.4 全局execute_data和opline
- 3.4 面向對象實現
- 3.4.1 類
- 3.4.2 對象
- 3.4.3 繼承
- 3.4.4 動態屬性
- 3.4.5 魔術方法
- 3.4.6 類的自動加載
- 3.5 運行時緩存
- 3.6 Opcache
- 3.6.1 opcode緩存
- 3.6.2 opcode優化
- 3.6.3 JIT
- 第4章 PHP基礎語法實現
- 4.1 類型轉換
- 4.2 選擇結構
- 4.3 循環結構
- 4.4 中斷及跳轉
- 4.5 include/require
- 4.6 異常處理
- 第5章 內存管理
- 5.1 Zend內存池
- 5.2 垃圾回收
- 第6章 線程安全
- 6.1 什么是線程安全
- 6.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.3.1 擴展的構成
- 7.3.2 編譯工具
- 7.3.3 編寫擴展的基本步驟
- 7.3.4 config.m4
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.5.1 全局變量
- 7.5.2 ini配置
- 7.6 函數
- 7.6.1 內部函數注冊
- 7.6.2 函數參數解析
- 7.6.3 引用傳參
- 7.6.4 函數返回值
- 7.6.5 函數調用
- 7.7 zval的操作
- 7.7.1 新生成各類型zval
- 7.7.2 獲取zval的值及類型
- 7.7.3 類型轉換
- 7.7.4 引用計數
- 7.7.5 字符串操作
- 7.7.6 數組操作
- 7.8 常量
- 7.9 面向對象
- 7.9.1 內部類注冊
- 7.9.2 定義成員屬性
- 7.9.3 定義成員方法
- 7.9.4 定義常量
- 7.9.5 類的實例化
- 7.10 資源類型
- 7.11 經典擴展解析
- 7.8.1 Yaf
- 7.8.2 Redis
- 第8章 命名空間
- 8.1 概述
- 8.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- break/continue按標簽中斷語法實現
- defer推遲函數調用語法的實現
- 一起線上事故引發的對PHP超時控制的思考