## 4.5 include/require
在實際應用中,我們不可能把所有的代碼寫到一個文件中,而是會按照一定的標準進行文件劃分,include與require的功能就是將其他文件包含進來并且執行,比如在面向對象中通常會把一個類定義在單獨文件中,使用時再include進來,類似其他語言中包的概念。
include與require沒有本質上的區別,唯一的不同在于錯誤級別,當文件無法被正常加載時include會拋出warning警告,而require則會拋出error錯誤,本節下面的內容將以include說明。
在分析include的實現過程之前,首先要明確include的基本用法及特點:
* 被包含的文件將繼承include所在行具有的全部變量范圍,比如調用文件前面定義了一些變量,那么這些變量就能夠在被包含的文件中使用,反之,被包含文件中定義的變量也將從include調用處開始可以被被調用文件所使用。
* 被包含文件中定義的函數、類在include執行之后將可以被隨處使用,即具有全局作用域。
* include是在運行時加載文件并執行的,而不是在編譯時。
這幾個特性可以理解為include就是把其它文件的內容拷貝到了調用文件中,類似C語言中的宏(當然執行的時候并不是這樣),舉個例子來說明:
```php
//a.php
$var_1 = "hi";
$var_2 = array(1,2,3);
include 'b.php';
var_dump($var_2);
var_dump($var_3);
//b.php
$var_2 = array();
$var_3 = 9;
```
執行`php a.php`結果顯示$var_2值被修改為array()了,而include文件中新定義的$var_3也可以在調用文件中使用。
接下來我們就以這個例子詳細介紹下include具體是如何實現的。

前面我們曾介紹過Zend引擎的編譯、執行兩個階段(見上圖),整個過程的輸入是一個文件,然后經過`PHP代碼->AST->Opcodes->execute`一系列過程完成整個處理,編譯過程的輸入是一個文件,輸出是zend_op_array,輸出接著成為執行過程的輸入,而include的處理實際就是這個過程,執行include時把被包含的文件像主腳本一樣編譯然后執行,接著在回到調用處繼續執行。

include的編譯過程非常簡單,只編譯為一條opcode:`ZEND_INCLUDE_OR_EVAL`,下面看下其具體處理過程:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
//include文件編譯的zend_op_array
zend_op_array *new_op_array=NULL;
zval *inc_filename;
zval tmp_inc_filename;
zend_bool failure_retval=0;
SAVE_OPLINE();
inc_filename = EX_CONSTANT(opline->op1);
...
switch (opline->extended_value) {
...
case ZEND_INCLUDE:
case ZEND_REQUIRE:
//編譯include的文件
new_op_array = compile_filename(opline->extended_value, inc_filename);
break;
...
}
...
zend_execute_data *call;
//分配運行時的zend_execute_data
call = zend_vm_stack_push_call_frame(ZEND_CALL_NESTED_CODE,
(zend_function*)new_op_array, 0, EX(called_scope), Z_OBJ(EX(This)));
//繼承調用文件的全局變量符號表
if (EX(symbol_table)) {
call->symbol_table = EX(symbol_table);
} else {
call->symbol_table = zend_rebuild_symbol_table();
}
//保存當前zend_execute_data,include執行完再還原
call->prev_execute_data = execute_data;
//執行前初始化
i_init_code_execute_data(call, new_op_array, return_value);
//zend_execute_ex執行器入口,如果沒有自定義這個函數則默認為execute_ex()
if (EXPECTED(zend_execute_ex == execute_ex)) {
//將執行器切到新的zend_execute_data,回憶下execute_ex()中的切換過程
ZEND_VM_ENTER();
}
...
}
```
整個過程比較容易理解,編譯的過程不再重復,與之前介紹的沒有差別;執行的過程實際非常像函數的調用過程,首先也是重新分配了一個zend_execute_data,然后將執行器切到新的zend_execute_data,執行完以后再切回調用處,如果include文件中只定義了函數、類,沒有定義全局變量則執行過程實際直接執行return,只是在編譯階段將函數、類注冊到EG(function_table)、EG(class_table)中了,這種情況比較簡單,但是如果有全局變量定義處理就比較復雜了,比如上面那個例子,兩個文件中都定義了全局變量,這些變量是如何被繼承、合并的呢?
上面的過程中還有一個關鍵操作:`i_init_code_execute_data()`,關于這個函數在前面介紹`zend_execute()`時曾提過,這里面除了一些上下文的設置還會把當前zend_op_array下的變量移到EG(symbol_table)全局變量符號表中去,這些變量相對自己的作用域是局部變量,但它們定義在函數之外,實際也是全局變量,可以在函數中通過global訪問,在執行前會把所有在php中定義的變量(zend_op_array->vars數組)插入EG(symbol_table),value指向zend_execute_data局部變量的zval,如下圖:

而include時也會執行這個步驟,如果發現var已經在EG(symbol_table)存在了,則會把value重新指向新的zval,也就是被包含文件的zend_execute_data的局部變量,同時會把原zval的value"拷貝"給新zval的value,概括一下就是被包含文件中的變量會繼承、覆蓋調用文件中的變量,這就是為什么被包含文件中可以直接使用調用文件中定義的變量的原因。被包含文件在`zend_attach_symbol_table()`完成以后EG(symbole_table)與zend_execute_data的關系:

> 注意:這里include文件中定義的var_2實際是替換了原文件中的變量,也就是只有一個var_2,所以此處zend_array的引用是1而不是2
接下來就是被包含文件的執行,執行到`$var_2 = array()`時,將原array(1,2,3)引用減1變為0,這時候將其釋放,然后將新的value:array()賦給$var_2,這個過程就是普通變量的賦值過程,注意此時調用文件中的$var_2仍然指向被釋放掉的value,此時的內存關系:

看到這里你可能會有一個疑問:$var_2既然被重新修改為新的一個值了,那么為什么調用文件中的$var_2仍然指向釋放掉的value呢?include執行完成回到原來的調用文件中后為何可以讀取到新的$var_2值以及新定義的var_3呢?答案在被包含文件執行完畢return的過程中。
被包含文件執行完以后最后執行return返回調用文件include的位置,return時會把***被包含文件中的***全局變量從zend_execute_data中移到EG(symbol_table)中,這里的移動是把value值更新到EG(symbol_table),而不是像原來那樣間接的指向value,這個操作在`zend_detach_symbol_table()`中完成,具體的return處理:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
{
...
if (EXPECTED((ZEND_CALL_KIND_EX(call_info) & ZEND_CALL_TOP) == 0)) {
//將include文件中定義的變量移到EG(symbol_table)
zend_detach_symbol_table(execute_data);
//釋放zend_op_array
destroy_op_array(&EX(func)->op_array);
old_execute_data = execute_data;
//切回調用文件的zend_execute_data
execute_data = EG(current_execute_data) = EX(prev_execute_data);
//釋放include文件的zend_execute_data
zend_vm_stack_free_call_frame_ex(call_info, old_execute_data);
//重新attach
zend_attach_symbol_table(execute_data);
LOAD_NEXT_OPLINE();
ZEND_VM_LEAVE();
}else{
//函數、主腳本返回的情況
}
}
```
`zend_detach_symbol_table()`操作:
```c
ZEND_API void zend_detach_symbol_table(zend_execute_data *execute_data)
{
zend_op_array *op_array = &execute_data->func->op_array;
HashTable *ht = execute_data->symbol_table;
/* copy real values from CV slots into symbol table */
if (EXPECTED(op_array->last_var)) {
zend_string **str = op_array->vars;
zend_string **end = str + op_array->last_var;
zval *var = EX_VAR_NUM(0);
do {
if (Z_TYPE_P(var) == IS_UNDEF) {
zend_hash_del(ht, *str);
} else {
zend_hash_update(ht, *str, var);
ZVAL_UNDEF(var);
}
str++;
var++;
} while (str != end);
}
}
```
完成以后EG(symbol_table):

接著是還原調用文件的zend_execute_data,切回調用文件的include位置,在將執行器切回之前再次執行了`zend_attach_symbol_table()`,這時就會將原調用文件的變量重新插入全局變量符號表,插入$var_2、$var_3時發現已經存在了,則將局部變量區的$var_2、$var_3的value修改為這個值,這就是$var_2被include文件更新后覆蓋原value的過程,同時$var_3也因為在調用文件中出現了所以值被修改為include中設定的值,此時的內存關系:

這就是include的實現原理,整個過程并不復雜,比較難理解的一點在于兩個文件之間變量的繼承、覆蓋,可以仔細研究下上面不同階段時的內存關系圖。
最后簡單介紹下include_once、require_once,這兩個與include、require的區別是在一次請求中同一文件只會被加載一次,第一次執行時會把這個文件保存在EG(included_files)哈希表中,再次加載時檢查這個哈希表,如果發現已經加載過則直接跳過。
- 目錄
- 第1章 PHP基本架構
- 1.1 PHP簡介
- 1.2 PHP7的改進
- 1.3 FPM
- 1.4 PHP執行的幾個階段
- 第2章 變量
- 2.1 變量的內部實現
- 2.2 數組
- 2.3 靜態變量
- 2.4 全局變量
- 2.5 常量
- 3.1 PHP代碼的編譯
- 3.1.1 詞法解析、語法解析
- 3.1.2 抽象語法樹編譯流程
- 第3章 Zend虛擬機
- 3.2.1 內部函數
- 3.2.2 用戶函數的實現
- 3.3 Zend引擎執行流程
- 3.3.1 基本結構
- 3.2 函數實現
- 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.2 線程安全資源管理器
- 第7章 擴展開發
- 7.1 概述
- 6.1 什么是線程安全
- 7.2 擴展的實現原理
- 7.3 擴展的構成及編譯
- 7.4 鉤子函數
- 7.5 運行時配置
- 7.6 函數
- 7.7 zval的操作
- 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.2 命名空間的定義
- 8.2.1 定義語法
- 8.2.2 內部實現
- 8.3 命名空間的使用
- 8.3.1 基本用法
- 8.3.2 use導入
- 8.3.3 動態用法
- 附錄
- 附錄1:break/continue按標簽中斷語法實現
- 附錄2:defer推遲函數調用語法的實現
- 8.1 概述