### 3.3.4 全局execute_data和opline
Zend執行器在opcode的執行過程中,會頻繁的用到execute_data和opline兩個變量,execute_data為zend_execute_data結構,opline為當前執行的指令。普通的處理方式在執行每條opcode指令的handler時,會把execute_data地址作為參數傳給handler使用,使用時先從當前棧上獲取execute_data地址,然后再從堆上獲取變量的數據,這種方式下Zend執行器展開后是下面這樣:
```c
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)execute_data->opline->handler)(execute_data)) != 0)) {
if (EXPECTED(ret > 0)) {
execute_data = EG(current_execute_data);
} else {
return;
}
}
}
}
```
執行器實際是一個大循環,從第一條opcode開始執行,execute_data->opline指向當前執行的指令,執行完以后指向下一條指令,opline類似eip(或rip)寄存器的作用。通過這個循環,ZendVM完成opcode指令的執行。opcode執行完后以后指向下一條指令的操作是在當前handler中完成,也就是說每條執行執行完以后會主動更新opline,這里會有下面幾個不同的動作:
```c
#define ZEND_VM_CONTINUE() return 0
#define ZEND_VM_ENTER() return 1
#define ZEND_VM_LEAVE() return 2
#define ZEND_VM_RETURN() return -1
```
ZEND_VM_CONTINUE()表示繼續執行下一條opcode;ZEND_VM_ENTER()/ZEND_VM_LEAVE()是調用函數時的動作,普通模式下ZEND_VM_ENTER()實際就是return 1,然后execute_ex()中會將execute_data切換到被調函數的結構上,對應的,在函數調用完成后ZEND_VM_LEAVE()會return 2,再將execute_data切換至原來的結構;ZEND_VM_RETURN()表示執行完成,返回-1給execute_ex(),比如exit,這時候execute_ex()將退出執行。下面看一個具體的例子:
```php
$a = "hi~";
echo $a;
```
執行過程如下圖所示:

以ZEND_ASSIGN這條賦值指令為例,其handler展開前如下:
```c
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
...
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
```
所有opcode的handler定義格式都是相同的,其參數列表通過ZEND_OPCODE_HANDLER_ARGS宏定義,展開后實際只有一個execute_data,展開后:
```c
static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(zend_execute_data *execute_data)
{
//USE_OPLINE
const zend_op *opline = execute_data->opline;
...
//ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION()
execute_data->opline = execute_data->opline + 1;
return 0;
}
```
從這個例子可以很清楚的看到,執行完以后會將execute_data->opline加1,也就是指向下一條opcode,然后返回0給execute_ex(),接著執行器在下一次循環時執行下一條opcode,依次類推,直至所有的opcode執行完成。這個處理過程比較簡單,并沒有不好理解的地方,而且整個過程看起來也都那么順理成章。PHP7針對execute_data、opline兩個變量的存儲位置進行了優化,那就是使用全局寄存器保存這兩個變量的地址,以實現更高效率的讀取。這種方式下execute_data、opline直接從寄存器讀取地址,在性能上大概有5%的提升(官方說法)。在分析PHP7的優化之前,我們先簡單介紹下什么是寄存器變量。
寄存器變量存放在CPU的寄存器中,使用時,不需要訪問內存直接從寄存器中讀寫,與存儲在內存中的變量相比,寄存器變量具有更快的訪問速度,在計算機的存儲層次中,寄存器的速度最快,其次是內存,最慢的是硬盤。C語言中使用關鍵字register來聲明局部變量為寄存器變量,需要注意的是,只有局部自動變量和形式參數才能夠被定義為寄存器變量,全局變量和局部靜態變量都不能被定義為寄存器變量。而且,一個計算機中寄存器數量是有限的,一般為2到3個,因此寄存器變量的數量不能太多。對于在一個函數中說明的多于2到3個的寄存器變量,C編譯程序會自動地將寄存器變量變為自動變量。 受硬件寄存器長度的限制,寄存器變量只能是char、int或指針型,而不能使其他復雜數據類型。由于register變量使用的是硬件CPU中的寄存器,寄存器變量無地址,所以不能使用取地址運算符"&"求寄存器變量的地址。
GCC從4.8.0版本開始支持了另外一項特性:全局寄存器變量(Global Register Variables,[詳細介紹](https://gcc.gnu.org/onlinedocs/gcc-6.1.0/gcc/Global-Register-Variables.html)),也就是可以把全局變量定義為寄存器變量,從而可以實現函數間共享數據。可以通過下面的語法告訴編譯器使用寄存器來保存數據:
```c
register int *foo asm ("r12"); //r12、%r12
```
或者:
```c
register int *foo __asm__ ("r12"); //r12、%r12
```
這里r12就是指定使用的寄存器,它必須是運行平臺上有效的寄存器,這樣就可以像使用普通的變量一樣使用foo,但是foo同樣沒有地址,也就是無法通過&獲取它的地址,在gdb調試時也無法使用foo符號,只能使用對應的寄存器獲取數據。舉個例子來看:
```c
//main.c
#include <stdlib.h>
typedef struct _execute_data {
int ip;
}zend_execute_data;
register zend_execute_data* execute_data __asm__ ("%r14");
int main(void)
{
execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data));
execute_data->ip = 9999;
return 0;
}
```
編譯:`$ gcc -o main -g main.c`,然后通過gdb看下:
```sh
$ gdb main
(gdb) break main
(gdb) r
Starting program: /home/qinpeng/c/php/main
Breakpoint 1, main () at main.c:12
12 execute_data = (zend_execute_data *)malloc(sizeof(zend_execute_data));
(gdb) n
13 execute_data->ip = 9999;
(gdb) n
15 return 0;
```
這時我們就無法再像普通變量那樣直接使用execute_data訪問數據,只能通過r14寄存器讀取:
```sh
(gdb) p execute_data
Missing ELF symbol "execute_data".
(gdb) info register r14
r14 0x601010 6295568
(gdb) p ((zend_execute_data *)$r14)->ip
$3 = 9999
```
了解完全局寄存器變量,接下來我們再回頭看下PHP7中的用法,處理也比較簡單,就是在execute_ex()執行各opcode指令的過程中,不再將execute_data作為參數傳給handler,而是通過寄存器保存execute_data及opline的地址,handler使用時直接從全局變量(寄存器)讀取,執行完再把下一條指令更新到全局變量。
該功能需要GCC 4.8+支持,默認開啟,可以通過 --disable-gcc-global-regs 編譯參數關閉。以x86_64為例,execute_data使用r14寄存器,opline使用r15寄存器:
```c
//file: zend_execute.c line: 2631
# define ZEND_VM_FP_GLOBAL_REG "%r14"
# define ZEND_VM_IP_GLOBAL_REG "%r15"
//file: zend_vm_execute.h line: 315
register zend_execute_data* volatile execute_data __asm__(ZEND_VM_FP_GLOBAL_REG);
register const zend_op* volatile opline __asm__(ZEND_VM_IP_GLOBAL_REG);
```
execute_data、opline定義為全局變量,下面看下execute_ex()的變化,展開后:
```c
ZEND_API void execute_ex(zend_execute_data *ex)
{
const zend_op *orig_opline = opline;
zend_execute_data *orig_execute_data = execute_data;
//將當前execute_data、opline保存到全局變量
execute_data = ex;
opline = execute_data->opline
while (1) {
((opcode_handler_t)opline->handler)();
if (UNEXPECTED(!opline)) {
execute_data = orig_execute_data;
opline = orig_opline;
return;
}
}
}
```
這個時候調用各opcode指令的handler時就不再傳入execute_data的參數了,handler使用時直接從全局變量讀取,仍以上面的賦值ZEND_ASSIGN指令為例,handler展開后:
```c
static int ZEND_ASSIGN_SPEC_CV_CONST_HANDLER(void)
{
...
//ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION()
opline = execute_data->opline + 1;
return;
}
```
當調用函數時,會把execute_data、opline更新為被調函數的,然后回到execute_ex()開始執行被調函數的指令:
```c
# define ZEND_VM_ENTER() execute_data = EG(current_execute_data); LOAD_OPLINE(); ZEND_VM_CONTINUE()
```
展開后:
```c
//ZEND_VM_ENTER()
execute_data = execute_data->current_execute_data;
opline = execute_data->opline;
return;
```
這兩種處理方式并沒有本質上的差異,只是通過全局寄存器變量提升了一些性能。
> __Note:__ automake編譯時的命令是cc,而不是gcc,如果更新gcc后發現PHP仍然沒有支持這個特性,請檢查下cc是否指向了新的gcc
- 前言
- 第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超時控制的思考