## 4.6 異常處理
PHP的異常處理與其它語言的類似,在程序中可以拋出、捕獲一個異常,異常拋出必須只有定義在try{...}塊中才可以被捕獲,捕獲以后將跳到catch塊中進行處理,不再執行try中拋出異常之后的代碼。
異常可以在任意位置拋出,然后將由最近的一個try所捕獲,如果在當前執行空間沒有進行捕獲,那么將調用棧一直往上拋,比如在一個函數內部拋出一個異常,但是函數內沒有進行try,而在函數調用的位置try了,那么就由調用處的catch捕獲。
接下來我們從兩個方面介紹下PHP異常處理的實現。
### 4.6.1 異常處理的編譯
異常捕獲及處理的語法:
```php
try{
try statement;
}catch(exception_class_1 $e){
catch statement 1;
}catch(exception_class_2 $e){
catch statement 2;
}finally{
finally statement;
}
```
try表示要捕獲try statement中可能拋出的異常;catch是捕獲到異常后的處理,可以定義多個,當try中拋出異常時會依次檢查各個catch的異常類是否與拋出的匹配,如果匹配則有命中的那個catch塊處理;finally為最后執行的代碼,不管是否有異常拋出都會執行。
語法規則:
```c
statement:
...
| T_TRY '{' inner_statement_list '}' catch_list finally_statement
{ $$ = zend_ast_create(ZEND_AST_TRY, $3, $5, $6); }
...
;
catch_list:
/* empty */
{ $$ = zend_ast_create_list(0, ZEND_AST_CATCH_LIST); }
| catch_list T_CATCH '(' name T_VARIABLE ')' '{' inner_statement_list '}'
{ $$ = zend_ast_list_add($1, zend_ast_create(ZEND_AST_CATCH, $4, $5, $8)); }
;
finally_statement:
/* empty */ { $$ = NULL; }
| T_FINALLY '{' inner_statement_list '}' { $$ = $3; }
;
```
從語法規則可以看出,try-catch-finally最終編譯為一個`ZEND_AST_TRY`節點,包含三個子節點,分別是:try statement、catch list、finally statement,try statement、finally statement就是普通的`ZEND_AST_STMT_LIST`節點,catch list包含多個`ZEND_AST_CATCH`節點,每個節點有三個子節點:exception class、exception object及catch statement,最終生成的AST:

具體的編譯過程如下:
* __(1)__ 向所屬zend_op_array注冊一個zend_try_catch_element結構,所有try都會注冊一個這樣的結構,與循環結構注冊的zend_brk_cont_element類似,當前zend_op_array所有定義的異常保存在zend_op_array->try_catch_array數組中,這個結構用來記錄try、catch以及finally開始的位置,具體結構:
```c
typedef struct _zend_try_catch_element {
uint32_t try_op; //try開始的opcode位置
uint32_t catch_op; //第1個catch塊的opcode位置
uint32_t finally_op; //finally開始的opcode位置
uint32_t finally_end;//finally結束的opcode位置
} zend_try_catch_element;
```
* __(2)__ 編譯try statement,編譯完以后如果定義了catch塊則編譯一條`ZEND_JMP`,此opcode的作用時當無異常拋出時跳過所有catch跳到finally或整個異常之外的,因為catch塊是在try statement之后編譯的,所以具體的跳轉值目前還無法確定;
* __(3)__ 依次編譯各個catch塊,如果沒有定義則跳過此步驟,每個catch編譯時首先編譯一條`ZEND_CATCH`,此opcode保存著此catch的exception class、exception object以及下一個catch塊開始的位置,編譯第1個catch時將此opcode的位置記錄在zend_try_catch_element.catch_op上,接著編譯catch statement,最后編譯一條`ZEND_JMP`(最后一個catch不需要),此opcode的作用與步驟(2)的相同;
* __(4)__ 將步驟(2)、步驟(3)中`ZEND_JMP`跳轉值設置為finally第1條opcode或異常定義之外的代碼,如果沒有定義finally則結束編譯,否則編譯finally塊,首先編譯一條`ZEND_FAST_CALL`及`ZEND_JMP`,接著編譯finally statement,最后編譯一條`ZEND_FAST_RET`。
編譯完以后的結構:

異常的拋出通過throw一個異常對象來實現,這個對象必須繼承>自Exception類,拋出異常的語法:
```php
throw exception_object;
```
throw的編譯比較簡單,最終只編譯為一條opcode:`ZEND_THROW`。
### 4.6.2 異常的拋出與捕獲
上一小節我們介紹了exception結構在編譯階段的處理,接下來我們再介紹下運行時exception的處理過程,這個過程相對比較復雜,整體的講其處理流程整體如下:
* __(1)__ 檢查拋出的是否是object,否則將導致error錯誤;
* __(2)__ 將EG(exception)設置為拋出的異常對象,同時將當前stack(即:zend_execute_data)接下來要執行的opcode設置為`ZEND_HANDLE_EXCEPTION`;
* __(3)__ 執行`ZEND_HANDLE_EXCEPTION`,查找匹配的catch:
* __(3.1)__ 首先遍歷當前zend_op_array下定義的所有異常捕獲,即`zend_op_array->try_catch_array`數組,然后根據throw的位置、try開始的位置、catch開始的位置、finally開始的位置判斷判斷異常是否在try范圍內,如果同時命中了多個try(即嵌套try的情況)則選擇最后那個(也就是最里層的),遍歷完以后如果命中了則進入步驟(3.2)處理,如果沒有命中當前stack下任何try則進入步驟(4);
* __(3.2)__ 到這一步表示拋出的異常在當前zend_op_array下有try攔截(注意這里只是表示異常在try中拋出的,但是拋出的異常并一定能被catch),然后根據當前try塊的`zend_try_catch_element`結構取出第一個catch的位置,將opcode設置為zend_try_catch_element.catch_op,跳到第一個catch塊開始的位置執行,即:執行`ZEND_CATCH`;
* __(3.3)__ 執行`ZEND_CATCH`,檢查拋出的異常對象是否與當前catch的類型匹配,檢查的過程為判斷兩個類是否存在父子關系,如果匹配則表示異常被成功捕獲,將EG(exception)清空,如果沒有則跳到下一個catch的位置重復步驟(3.3),如果到最后一個catch仍然沒有命中則在這個catch的位置拋出一個異常(實際還是原來按個異常,只是將拋出的位置轉移了當前catch的位置),然后回到步驟(3);
* __(4)__ 當前zend_op_array沒能成功捕獲異常,需要繼續往上拋:回到調用位置,將接下來要執行的opcode設置為`ZEND_HANDLE_EXCEPTION`,比如函數中拋出了一個異常沒有在函數中捕獲,則跳到調用的位置繼續捕獲,回到步驟(3);如果到最終主腳本也沒有被捕獲則將結束執行并導致error錯誤。

這個過程最復雜的地方在于異常匹配、傳遞的過程,主要為`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`兩條opcode之間的調用,當拋出一個異常時會終止后面opcode的執行,轉向執行`ZEND_HANDLE_EXCEPTION`,根據異常拋出的位置定位到最近的一個try的catch位置,如果這個catch沒有匹配則跳到下一個catch塊,然后再次執行`ZEND_HANDLE_EXCEPTION`,如果到最后一個catch仍沒有匹配則將異常拋出前位置EG(opline_before_exception)更新為最后一個catch的位置,再次執行`ZEND_HANDLE_EXCEPTION`,由于異常拋出的位置已經更新了所以不會再匹配上次檢查過的那個catch,這個過程實際就是不斷遞歸執行`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`;如果當前zend_op_array都無法捕獲則將異常拋向上一個調用棧繼續捕獲,下面根據一個例子具體說明下:
```php
function my_func(){
//...
throw new Exception("This is a exception from my_func()");
}
try{
my_func();
}catch(ErrorException $e){
echo "ErrorException";
}catch(Exception $e){
echo "Exception";
}
```
my_func()中拋出了一個異常,首先在my_func()中拋出一個異常,然后在my_func()的zend_op_array中檢查是不是能夠捕獲,發現沒有,則回到調用的位置,再次檢查,第1次匹配到`catch(ErrorException $e)`,檢查后發現并不匹配,然后跳到下一個catch塊繼續匹配,第2次匹配到`catch(Exception $e)`,檢查后發現命中,捕獲成功。

上面的過程并沒有提到finally的執行時機,首先要明確finally在哪些情況下會執行,命中catch的情況比較簡單,即在catch statement執行完以后跳到finally執行,另外一種情況是如果一個異常在try中但沒有命中任何catch那么其finally也是會被執行的,這種情況的finally實際是在步驟(3)中執行的,最后一個catch檢查完以后會更新異常拋出位置:EG(opline_before_exception),然后會再次執行`ZEND_HANDLE_EXCEPTION`,再次檢查時就會發現沒有命中任何catch但命中finally了(因為異常位置更新了),這時候就會將異常對象保存在finally塊中,然后執行finally,執行完再將異常對象還原繼續捕獲,下面看下步驟(3)的具體處理過程:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_HANDLE_EXCEPTION_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
//op_num為異常拋出的位置,根據異常拋出前最后一條opcode與第一條opcode計算得出
uint32_t op_num = EG(opline_before_exception) - EX(func)->op_array.opcodes;
uint32_t catch_op_num = 0, finally_op_num = 0, finally_op_end = 0;
//查找異常是不是被try了:找最近的一層try
for (i = 0; i < EX(func)->op_array.last_try_catch; i++) {
if (EX(func)->op_array.try_catch_array[i].try_op > op_num) {
//try在拋出之后
break;
}
in_finally = 0;
//異常拋出位置在try后且比第一個catch位置小,表明這個try有可能捕獲異常
if (op_num < EX(func)->op_array.try_catch_array[i].catch_op) {
//第一個catch的位置
catch_op_num = EX(func)->op_array.try_catch_array[i].catch_op;
}
//當前try有finally
if (op_num < EX(func)->op_array.try_catch_array[i].finally_op) {
finally_op_num = EX(func)->op_array.try_catch_array[i].finally_op;
finally_op_end = EX(func)->op_array.try_catch_array[i].finally_end;
}
if (op_num >= EX(func)->op_array.try_catch_array[i].finally_op &&
op_num < EX(func)->op_array.try_catch_array[i].finally_end) {
finally_op_end = EX(func)->op_array.try_catch_array[i].finally_end;
in_finally = 1;
}
}
cleanup_unfinished_calls(execute_data, op_num);
//異常命中了try但沒有命中任何catch且那個try定義了finally:需要執行finally
//catch_op_num >= finally_op_num是嵌套try的情況,因為finally是檢查完所有catch、更新異常拋出位置之后再執行的
//所以檢查完內層try再檢查外層循環時會出現這種情況
if (finally_op_num && (!catch_op_num || catch_op_num >= finally_op_num)) {
zval *fast_call = EX_VAR(EX(func)->op_array.opcodes[finally_op_end].op1.var);
cleanup_live_vars(execute_data, op_num, finally_op_num);
if (in_finally && Z_OBJ_P(fast_call)) {
zend_exception_set_previous(EG(exception), Z_OBJ_P(fast_call));
}
//臨時將EG(exception)轉移到finally下,執行完finally再拋出
Z_OBJ_P(fast_call) = EG(exception);
EG(exception) = NULL;
fast_call->u2.lineno = (uint32_t)-1;
ZEND_VM_SET_OPCODE(&EX(func)->op_array.opcodes[finally_op_num]);
ZEND_VM_CONTINUE();
}else{
//這個是善后處理,因為異常拋出后后面的opcode將不再執行,但有些情況下還需要把一些資源釋放掉
//比如前面我們介紹goto時提到的foreach中是不能直接跳出的,throw也是類似
cleanup_live_vars(execute_data, op_num, catch_op_num);
...
if (catch_op_num) {
//匹配到catch(但不一定命中),跳到catch處執行ZEND_CATCH進行判斷
ZEND_VM_SET_OPCODE(&EX(func)->op_array.opcodes[catch_op_num]);
ZEND_VM_CONTINUE();
} else if (UNEXPECTED((EX(func)->op_array.fn_flags & ZEND_ACC_GENERATOR) != 0)) {
...
} else {
//當前zend_op_array下已經沒有匹配到的try了,如果異常仍沒有被捕獲則將在zend_leave_helper_SPEC()將異常拋給prev_execute_data繼續捕獲
ZEND_VM_TAIL_CALL(zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
}
}
}
```
具體的實現過程還有很多額外的處理,這里不再展開,感興趣的可以詳細研究下`ZEND_HANDLE_EXCEPTION`、`ZEND_CATCH`兩條opcode以及zend_exception.c中具體邏輯。
### 4.6.3 內核的異常處理
前面介紹的異常處理是PHP語言層面的實現,在內核中也有一套供內核使用的異常處理模型,也就是C語言異常處理的實現,如:
```c
static int php_start_sapi(void)
{
...
zend_try {
...
} zend_catch {
...
} zend_end_try();
...
}
```
C語言并沒有在語言層面提供try-catch機制,那么PHP中的是如何實現的呢?這個主要利用sigsetjmp()、siglongjmp()兩個函數實現堆棧的保存、還原,在try的位置通過sigsetjmp()將當前位置的堆棧保存在一個變量中,異常拋出通過siglongjmp()跳回原位置,具體看下這幾個宏的定義:
```c
#define zend_try \
{ \
JMP_BUF *__orig_bailout = EG(bailout); \
JMP_BUF __bailout; \
\
EG(bailout) = &__bailout; \
if (SETJMP(__bailout)==0) {
#define zend_catch \
} else { \
EG(bailout) = __orig_bailout;
#define zend_end_try() \
} \
EG(bailout) = __orig_bailout; \
}
# define JMP_BUF sigjmp_buf
# define SETJMP(a) sigsetjmp(a, 0)
# define LONGJMP(a,b) siglongjmp(a, b)
# define JMP_BUF sigjmp_buf
```
展開后:
```c
{
//保存上一個zend_try記錄的JMP_BUF,目的是實現多層嵌套try
JMP_BUF *__orig_bailout = EG(bailout);
JMP_BUF __bailout;
//將當前堆棧保存在__bailout
EG(bailout) = &__bailout;
if (SETJMP(__bailout)==0) {
//try中的代碼
//拋出異常調用:LONGJMP()
}else { //異常拋出后到這個分支
EG(bailout) = __orig_bailout;
}
EG(bailout) = __orig_bailout;
}
```
- 前言
- 第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超時控制的思考