#### 第17章:
#### PHP底層設計與內核剖析
#### 17.1 初識PHP7源碼整體框架
##### 解釋型語言與編譯型語言
大多數高級語言是編譯型語言。編譯指在程序運行之前就將程序"翻譯"成匯編語言,然后再根據軟硬件環境編譯成目標文件。編譯器負責編譯工作。
PHP是`解釋型語言`。所謂解釋型語言就是在程序運行時才被"翻譯"成及其語言。由于執行一次"翻譯"一次,所以運行效率較低。解釋器負責"翻譯"源代碼的程序。
編譯型語言如何變成可執行的二進制文件?例如系統原理章節所講的C語言:
1. C語言代碼通過預處理器預處理。
2. 編譯器將C語言程序優化生成目標匯編程序。
3. 匯編器將目標匯編程序匯編成目標程序。
4. 鏈接器將共享文件、靜態庫函數等進行鏈接生成可執行的二進制文件。
編譯型語言的編譯結果文件已經是針對當前CPU體系的指令。
解釋性語言需先編譯成中間代碼,再由解釋型語言的特定虛擬機翻譯成CPU體系的指令被執行。所以解釋型語言是在運行過程中被翻譯成目標平臺的指令。
PHP程序被翻譯并執行的過程:
1. 運行過程中源代碼首先通過詞法分析器將代碼切割成多個Token字符串單元(Token有100多個,是有固定意義的字符串,對PHP開發者是無感的,因為它由詞法分析器處理)。
| Token標識符 | Code | 含義 |
| -------------- | ---- | ------------ |
| T_REQUIRE_ONCE | 258 | require_once |
| T_INCLUDE_ONCE | 261 | include_once |
| T_INCLUDE | 262 | include |
| T_LOGICAL_OR | 263 | or |
| T_LOGICAL_XOR | 264 | xor |
| T_LOGICAL_AND | 265 | and |
| T_PRINT | 266 | print |
| T_YIELD | 267 | yield |
? 部分Token含義
2. 由于一個個獨立的Token是無法表達完整語義的,此時由語法分析器將Token集合轉換為`抽象語法樹AST`。語法分析器基于Bison實現,使用了BNF來表達語法規則,Bison借助狀態機、狀態轉移表、壓棧、出棧等一系列操作生成了抽象語法樹,這樣保證了指令的運行順序。
3. 最后解釋器將抽象語法樹AST轉換為一條條機器指令opcode(opcode是有固定意義的字符串,對PHP開發者是無感的,因為它由解釋器生成)。
| opcode | Code | 含義 |
| -------- | ---- | ---------------------------------------------- |
| ZEND_NOP | 0 | 空操作,空函數或者其他無用操作會化作這個opcode |
| ZEND_ADD | 1 | 加 |
| ZEND_SUB | 2 | 減 |
| ZEND_MUL | 3 | 乘 |
| ZEND_DIV | 4 | 除 |
? 部分opcode指令的含義
4. 最后將opcodes交給Zend虛擬機依次執行(Excution函數開始執行)。
:-: 
? 解釋型語言執行示意
#### 17.2 PHP7內核架構
##### 1. Zend 引擎
詞法/語法分析,ATS生成和opcodes執行都在Zend引擎中實現。Zend引擎為PHP提供基礎服務,Zend引擎同時支持擴展。此外PHP的變量設計管理、內存管理、進程管理等都在引擎層實現。
##### 2. PHP層
PHP層負責與外界進行交互。與SAPI層的server交互。
##### 3. SAPI層
遵循PHP的輸入/輸出規范與PHP交互的一方稱為server。常見的是CLI SAPI、FPM API。只要遵循定義好的SAPI協議就可以完成交互,極大豐富了PHP支持的server類型。與PHP層交互。
##### 4. Zend擴展
Zend引擎提供了核心能力和接口規范。以此基礎開發的擴展為PHP代碼的性能和功能的多樣性提供了更豐富的選項。
:-: 
? PHP源碼架構
#### 17.3 PHP7源碼結構(未編譯)
##### 1. sapi 目錄
sapi目錄是對輸入輸出層的抽象,是PHP對外提供服務的規范。
輸入可以是來自cgi/fastcgi協議的網絡請求或者命令行;輸出可以是cgi/fastcgi協議的響應或寫到命令行的輸出。
sapi規范支持多種場景的交互,豐富了PHP的運行模式。
PHP的運行模式:
- ClI命令行模式:對應bin/php二進制程序文件執行,Swoole模式下的運行PHP也是這樣的模式。
- 內置模塊模式:PHP作為普通函數供Apache或任意C/C++程序調用,不需要二進制文件程序啟動。
- CGI模式:對應bin/cgi二進制程序文件執行,通常匹配Apache或IIS服務器(windows系統平臺下的Web服務器)。
- FastCGI模式:對應sbin/php-fpm二進制程序文件執行,現在大部分PHP系統都是運行在此模式下,通常用于匹配Nginx服務器。由Nginx這個server遵循PHP的SAPI輸入/輸出規范,通過FastCGI協議與PHP交互。
##### 2. Zend目錄
Zend目錄是PHP的核心代碼。主要有:
- 內存管理模塊:實現PHP的內存管理。
- 垃圾回收:解決循環引用問題和處理垃圾。
- 數組實現:主要是對PHP數組的實現。
...
##### 3. main目錄
main目錄是SAPI層和Zend層的粘合劑。
Zend層實現PHP腳本的編譯和執行,sapi層實現了輸入/輸出的抽象。mian目錄起到承上啟下的作用:解析SAPI的請求,分析要執行的腳本文件和參數;調用Zend引擎前完成初始化工作。比如php_execute_script()方法,它是PHP腳本的通用入口,可以在main目錄找到。
##### 4.ext目錄
ext目錄是PHP擴展目錄,常用的擴函數比如str、array、pdo等都在這里定義。
##### 5. TSRM目錄
線程安全資源管理器源碼目錄。早期的PHP都是單進程、單線程模型運行。現在大多也是用FastCGI模式下的PHP-FPM進程管理器運行的多進程模式。實際開發中PHP少有多線程場景。可以其編譯以支持線程安全,就可以用PHP的多線程擴展進行開發。TSRM給每個線程提供了獨立的全局變量副本做到線程之間就算在同一個進程內也完全獨立。具體實現是TSRM為線程分配一個獨立ID作為當前線程的全局變量內存區索引,這樣每個線程就可以使用自己區域的變量。
#### 17.4 生命周期
本章字符解釋:
- SG:請求信息表
- EG:存儲php.ini配置的符號表
PHP的整個生命周期被劃分為以下幾個階段:模塊初始化階段、請求初始化階段、腳本執行階段、請求關閉階段、模塊關閉階段。根據不同的SAPI的實現各階段的執行情況有一些差異。比如CLI模式下每次執行都會完整經歷這些階段,而FastCGI模式下只有第一次啟動時執行一次模塊初始化階段,之后的每個請求到PHP只會經歷請求初始化階段、執行腳本階段、請求關閉階段,而在SAPI關閉時才會執行一次模塊關閉階段。
:-: 
##### 1. 模塊初始化階段
進行PHP框架,Zend引擎初始化操作。入口函數為php_module_startup()。一般只在SAPI啟動階段執行一次,對于FPM而言就是FPM的master進程啟動時執行。這個階段的主要處理:
- 激活SAPI:初始化請求信息SG表、設置POST請求的handler回調函數等。模塊初始化階段處理完成后調用sapi_deactivate()析構函數。
- 啟動PHP輸出:php_output_startup()。
- 初始化垃圾回收器:gc_globals_ctor(),分配zend_gc_globals內存(Zend主機處理垃圾的內存區域)。
- 啟動Zend引擎:zend_startup()。
- 注冊PHP定義的一些常量:PHP_VERSION、PHP_ZTS、PHP_SAPI等。
- 解析php.ini。
- 將對PHP、Zend的核心php.ini配置加入EG哈希表中。
- 注冊獲取超全局變量的handler回調函數。例如獲取$_POST,$_GET。
- 注冊靜態編譯擴展:php_register_internal_extension_func(),將不需編譯PHP自帶的靜態擴展模塊加入到PHP中。
- 注冊動態加載模塊:php_ini_register_extensions(),將經過編譯或者無需編譯的php.ini中配置允許的動態擴展加入到PHP中。
- 回調各擴展定義的handler回調函數。
- 注冊php.ini中禁用的函數、類:disable_functions、disable_classes。
總結:模塊初始化階段主要負責激活SAPI,初始化超全局變量,注冊PHP內部的常量,啟動Zend引擎,解析php.ini配置文件,處理配置文件的配置,加載擴展到PHP中。
##### 2.請求初始化階段
每一個請求處理前都會經歷請求初始化階段。對于FPM就是worker進程accept一個請求,讀取、解析完請求數據后的一個階段。主要處理:
- 激活輸出:php_output_activate()。
- 激活Zend引擎:zend_activate()。主要操作:
- 重置垃圾回收器:gc_reset()。
- 初始化編譯器。
- 初始化執行器。
- 初始化詞法掃描器。
- 激活SAPI:sapi_activate()。
- 回調擴展定義的handler回調函數。
##### 3.執行腳本階段
由Zend引擎編譯、執行PHP代碼。執行階段入口函數為php_execute_script()。
##### 4. 請求關閉階段
PHP腳本解釋執行后進入請求關閉階段,這個階段將flush(flush指將緩沖區的數據沖刷出去,也就是輸出)輸出內容、發送HTTP響應頭、清理全局變量、關閉編譯器、關閉執行器等。
##### 5. 模塊關閉階段
SAPI關閉時執行模塊關閉階段,與模塊初始化階段對應作用相反。主要進行資源的清理,PHP各個模塊的關閉操作。
#### 17.5 PHP的內存管理
PHP底層是由C語言寫的,但是卻不需要像C語言手動分配、釋放內存空間。由內核幫我們實現了內存管理,包括分配回收。
##### 變量的自動GC機制
現代高級語言普遍提供了變量的自動GC機制,由語言自行管理內存,使得開發者可以不再關心內存的分配和釋放。比如PHP由`$`聲明一個變量,不需要手動銷毀,內核清除在什么時候進行釋放。
自動GC的簡單實現:在函數定義變量時分配一塊內存,用于存儲變量的結構(在PHP中是zval結構體的value),在函數返回時再將內存釋放,如果函數執行期間該變量作為參數調用了其他函數或賦值給了其他變量,則把變量復制一份,變量之間相互獨立不出現沖突。但是這樣相同值的變量占用空間會越來越多導致內存浪費。解決這個問題的方案:`引用計數+寫時復制`。當變量賦值、傳遞時不是直接進行深拷貝,而是多個變量共同使用一個value,用引用計數來記錄value由多少個變量在使用,他們都指向這個value值的空間;當某個變量的value需要發生改變無法與其他變量公用value時,將其深拷貝分離value,這就是寫時復制。
##### 引用計數
在PHP的變量結構體中有引用計數保存在了zend_value中。不同的數據類型結構里都有一個相同的成員:gc。用來保存引用計數,它的類型是zend_refcounted.h。
并不是所有的數據類型都會用到引用計數,只有復雜數據類型(有value結構的數據類型,例如字符串、數組)會用到引用計數。比如整型、浮點型、布爾型、NULL、內部字符串、不可變數組,他們的值直接通過zval保存,他們的會通過深拷貝賦值。PHP的局部變量的zval分配在zend_execute_data結構上。
```
typedef struct _zend_refcounted_h{
//引用計數
unit32_t refcount;
union{
struct{
ZEND_ENDIAN_LOHI_3(
//類型
zend_uchar type,
zend_uchar flags,
//垃圾回收時使用
uint16_t gc_info)
}v;
unit32_t_type_info;
}u;
}zend_refcounted_h;
```
例子:
```
$a = array(); //$a ->zend_array(refcount=1)
$b = $a; //$a,$b ->zend_array(refcount=2)
$c = $b; //$a,$b,$c ->zend_array(refcount=3)
unset($b); //$a,$b ->zend_array(refcount=2)
```
當unset解開變量$b與這個值之間綁定的關系,$b已不再在這個value的引用計數中。
例子:
```
$a = 1; //$a ->zend_long(refcount = 0)
$b = $a; //$b ->zend_long(refcount = 0)
```
簡單類型的值被直接存儲在zval中不會使用到引用計數。
內部字符串如PHP內部定義的字面量"hi",它在整個聲明周期都是不變的,所以不會使用引用計數。
不可變數組是zend引擎擴展opcache優化出來的。
內部字符串和普通字符串的類型都是IS_STRING,以及其他不同的情況如何區分字符串是否可以使用引用計數呢?
答案是所有PHP變量都遵從zval.u1結構體中的類型掩碼type_flag,它可以標識這個value是否可以使用引用計數。
##### 寫時復制
只有在必要的時候(即將發生寫的時候)才發生深拷貝,其余時候將空間共享可以有效提升效率。寫時復制指資源的復制需要在寫的時候才進行,在此之前以只讀的方式共享資源。
PHP中,當多個變量使用了引用計數后,其中一個變量修改value的情況時。發生修改的變量會復制一份數據來進行修改。同時斷開原來的value指向,指向新的value。
例子:
```
$a = [1,2];
$b = &$a;
$c = $a;
//發生分離,此時$a的變量的引用計數由3降為2
$c[] = 2;
```
PHP中對象、資源無法復制,所以不能做寫時復制。事實上只有string、array兩種數據類型支持value分離,與引用計數相同,這個信息也通過zval.u1.type_flag記錄,有copyable屬性的變量可以使用寫時賦值。
另外的情況時,如果變量從literals靜態數據區復制到局部變量區,會從literrals中深拷貝一份數據進行賦值。比如$a=array(),賦值時發現value:array()支持copyable,會從literals中深拷貝一份數據進行賦值。再比如$a = 'hi',value:'hi'是內部字符串,不支持copyable,$a會直接指向literals中的value。
##### 回收時機
在自動GC機制中,在zval斷開value的指向時候發現refount=0時則會釋放value。也可以使用unset()主動斷開變量的引用。
##### 垃圾回收
PHP7中垃圾回收的實現方式是定期遍歷和標記若干存儲對象的數組,再通過算法將是垃圾的物理空間回收。
垃圾回收包括`垃圾收集器`和`垃圾回收算法`。
垃圾回收器將可能是垃圾的元素收集在回收池,然后交垃圾回收算法回收。
例如:
```
$a = array(1);
$a = &$a;
unset($a);
```
在unset()之前,變量a的引用計數來自$a和$a[1]。unset()之后$a與value斷了聯系。但是$a的引用計數沒有由2變為0,而是1。此時就產生了垃圾。我們直到GC回收內存空間時是refount=0時。這就需要垃圾回收進行判斷回收這個空間了。
##### 內存池
PHP自己實現了一套內存池,用于替換類似C語言的malloc、free操作,以解決內存頻繁分配、釋放的問題。主要作用:減少內存分配及釋放的次數、有效控制內存碎片的產生。
內存池定義了三種粒度的內存塊:`chunk`、`page`、`slot`。每個chunk為2MB,page為4KB。每個chunk被分為512個page。而每個page被分為若干個slot。PHP在申請內存時按照不同的申請大小決定申請策略。
申請策略:
- Huge:申請內存大小大于2044KB,直接調用系統分配,分配若干個chunk。
- Large(page):申請內存大于3092B(即3/4大小page),小于2044KB(即511個page大小),分配若干個page。
- Small(slot):申請內存小于3092B,內存池提前定義好了30種同等大小內存(8,16,24,32...3072B),被分配在不同的page上,申請內存時直接在對應的page上查找可用的slot。
大內存分配實際上是若干個chunk,通過一個zend_mm_huge_list的鏈表進行管理。大內存之間構成一個單向鏈表。
chunk是內存池向系統申請、釋放內存的最小粒度,chunk之間構成雙向鏈表。
chunk中的第一個page存儲chunk的結構體,用于記錄chunk的相關信息,如前后chunk指針,page使用情況。
分配slot時會申請對應數量的page,然后會分配page,page之間組成鏈表。
相同大小的slot之間會構成單向鏈表。
#### 17.6 變量
PHP7的變量由zval結構體實現。其中value不再存儲復雜數據類型(數組、字符串、對象)。
```
struct_zval_struct{
zend_value value;
union{
struct{
ZEND_ENDIAN_LOHI_4(
zend_uchar type, //表明zval類型
zend_uchar type_flags,
zend_uchar const_flags,
zend_uchar reserved)
}v;
uint32_t type_info;
}u1;
union{
uint32_t next; //解決哈希沖突
uint32_t cache_slot; //運行時緩存
uint32_t lineno; //對于zend_ast_zval存行號
uint32_t num_args; //EX(this) 參數個數
uint32_t fe_pos; //foreach 的位置
uint32_t fe_iter_idx; //foreach 游標的標記
uint32_t access_flags; //類的常量訪問標識
uint32_t property_guard; //單一屬性保護
}u2;
};
```
其中value字段的結構如下:
```
typedef union _zend_value{
zend_long lval; //整型
double dval; //浮點
zend_refounted *counted; //引用計數
zend_string *str; //字符串類型
zend_array *arr; //數組類型
zend_object *obj; //對象類型
zend_resource *res; //資源類型
zend_reference *ref; //引用類型
zend_ast_ref *ast; //抽象語法樹
zval *zv; //zval類型
void *ptr; //指針類型
zend_class_entry *ce; //class類型
zend_function *func; //function類型
struct{
uint32_t w1;
uint32_t w2;
}ww;
}zend_value;
```
##### PHP7變量類型
指的是PHP7運行時底層的數據類型,不是指我們開發時的PHP變量的數據類型。由20種宏區分,用來標記zval結構體中的u1.type字段。
```
#define IS_UNDEF 0 //標記未使用類型
#define IS_NULL 1 //NULL
#define IS_FALSE 2 //布爾false
#define IS_TRUE 3 //布爾true
#define IS_LONG 4 //長整型
#define IS_DOUBLE 5 //浮點型
#define IS_STRING 6 //字符串
#define IS_ARRAY 7 //數組
#define IS_OBJECT 8 //對象
#define IS_RESOURCE 9 //資源類型
#define IS_REFERENCE 10 //參考類型(內部使用)
#define IS_CONSTANT 11 //常量類型
#define IS_CONSTANT_AST 12 //常量類型AST樹
/*偽類型*/
#define _IS_BOOL 13
#define IS_CALLABLE 14
#define IS_ITERABLE 18
#define IS_VOID 19
/*內部偽類型*/
#define IS_INDIRECT 15 //間接類型
#define IS_PTR 17 //指針類型
#define _IS_ERROR 20 //錯誤類型
```
##### 整型和浮點型
對于整型和浮點型,由于其占用空間小,在zval中是直接存儲的。
```
$a = 10; //$a 的zval_1(u1.v.type=IS_LONG,value.lval=10)
```
##### 字符串類型
字符串類型是特殊類型,zval的value指向字符串類型的結構體:
```
struct _zend_string{
zend_refcounted_h gc; //GC機制,用于引用計數、寫實分離和回收
zend_ulong h; //hash value
size_t len; //字符串長度
char val[1]; //字符串內容,采用柔性數組
}
```
##### 引用
zend_reference類型由記錄gc信息的zend_refcounted_h結構體和zval結構體組成。
```
struct _zend_reference}{
zend_refcounted_h gc;
zval val;
}
```
例如:
```
$a = 'hello'; //引用計數為1
$b = $a; //引用計數為2
$c = &b; //引用計數為2
```
當使用`&`操作時,會創建一種新的中間結構體zend_reference,指向真正的zend_string結構體,所以zend_string結構體的引用計數不變。同時zend_reference的引用計數變為2。此時$b和$c的類型都會變為zend_reference。這樣的好處是讓原始的zend_string在內存中只有一份。
##### 間接zval
依照上面的引用章節。$a 就是直接zval,value指向zend_string。而$b和$c結構體的value指向zend_reference。再由間接zval的value再指向zend_string。此時的zend_reference就**間接zval**。
##### 數組類型
PHP的數組類型是非常強大的數據結構。實際開發會大量使用。PHP使用哈希表(HashTable)存儲數組,哈希表是利用哈希函數將特定的鍵映射到特定的值得一種數據結構,它維護著鍵和值的一一對應的關系,并且可以根據鍵快速檢索到值,查詢效率為O(1)。PHP利用哈希函數和鏈地址法將數組bucket塊通過鏈表鏈接在一起并解決了哈希沖突。
PHP7數組的bucket塊鏈表是邏輯上的鏈表,所有的bucket塊都分配在連續的數組內存中,不再通過指針維護上下游關系,每個bucket塊只維護下一個bucket塊在數組中的索引(因為是連續內存,通過索引能夠快速定位到bucket)。
bucket又分為:
- 有效bucket:已存儲了元素值、數字索引和關聯索引。
- 無效bucket:已失效的bucket,使用時會使用它維護的下一個bucket索引跳過它。
- 未使用bucket:還未存儲任何數據。
:-: 
? PHP7數組bucket分類
bucket的結構體:
```
typedef struct _Bucket{
zval val; //可以存儲所有類型,甚至存儲其他數組
zend_ulong h;//hash value 對應數字key或者字符串key值,數字數組和關聯數組這里會保存其數字Key
zend_string *key;//表示字符串key,關聯數組這里會保存字符Key,數字數組這里會保存為NULL
}
```