【PHP7源碼分析】PHP7語言的執行原理

順風車運營研發團隊 李志 發表在程序人生 公衆號
咱們經常使用的高級語言有不少種,比較出名的有CC++、Python、 PHP、Go、Pascal等。而這些語言根據運行的方式不一樣,大致分爲兩種:編譯型語言和解釋型語言。php

其中,編譯型語言包括CC++、Pascal、Go等。這裏說的編譯是指在應用源程序執行以前,就將程序源代碼「翻譯」成彙編語言,而後進一步根據軟硬件環境編譯成目標文件。通常咱們稱完成編譯工做的工具叫編譯器。而解釋型語言,在程序運行時才被「翻譯」爲機器語言。可是執行一次「翻譯」一次,因此執行效率較低。解釋器的工做就是解釋性語言中,負責「翻譯」源代碼的程序。git

下面咱們更詳細地討論一下編譯型語言和解釋性語言的運行方式。github

1、編譯型語言與解釋型語言編程

咱們知道,對於一段C語言代碼,須要通過預編譯、編譯、彙編和連接,才能成爲可執行的二進制文件。以hello.c爲例:數組

#include<stdio.h>
int main(){
   printf("hello world");
   return 1;
}

對於這段C代碼,main是程序入口函數,實現的功能是打印字符串「hello world」 到屏幕上。編譯和執行過程如圖1所示。緩存

clipboard.png
圖1 編譯型語言的執行示意圖php7

第1步:C語言代碼預處理(好比依賴處理、宏替換等)。如以上代碼示例,#inlcude<stdio.h>就會在預處理階段被替換。架構

第2步:編譯。編譯器會把C語言翻譯成彙編語言程序,一條C語言一般便覺得多條彙編代碼。同時編譯器會對程序進行優化,生成目標彙編程序。函數

第3步:編譯獲得的彙編語言經過彙編器再彙編成目標程序hello.o。工具

第4步:連接。程序中每每包含一些共享目標文件,如示例程序中的printf()函數,位於靜態庫,須要通過連接器(如Uinx鏈接器ld)進行連接。

以C語言爲表明的編譯型語言,代碼發生更新都要通過以上步驟:

咱們區別編譯型語言與解釋型語言,主要立足於源代碼被編譯成目標平臺CPU指令的時機。對於編譯型語言,編譯結果已是針對當前CPU體系的指令;而解釋型語言,須要先編譯成中間代碼,再經由該解釋型語言的特定虛擬機,翻譯成特定CPU體系的指令被執行。解釋型語言是在運行過程當中,翻譯爲目標平臺的指令。常說解釋型語言「慢」,主要也是慢在這裏。

在PHP7中,源代碼首先將進行詞法分析,將源代碼切割爲多個字符串單元,分割後的字符串稱之爲Token。而一個個獨立的Token沒法表達完整語義,需通過語法分析階段,將Token轉換爲抽象語法樹(簡稱AST)。以後,抽象語法樹被轉換爲機器指令執行。在PHP中,這些指令稱爲opcode(後文會對opcode作更詳細的解釋,此處讀者能夠看待爲CPU指令)。

到AST的生成這一步,編譯型語言與解釋型語言所需經歷的過程類似。從抽象語法樹以後開始產生差別。

圖2是PHP(如無特殊說明,本章提到的PHP均爲PHP7版本)代碼被執行的簡化步驟,其中最後一步的左側分支,是編譯型語言的過程。

clipboard.png

圖2 以PHP爲例解釋型語言的執行示意圖

第1步:源碼經過詞法分析獲得Token;

第2步:基於語法分析器生成抽象語法樹(AST);

第3步:抽象語法樹轉換爲Opcodes(opcode指令集合),PHP解釋執行Opcodes。

接下來咱們在基本步驟的基礎上,細化PHP語言的執行原理,試圖更清晰地創建認知。

2、PHP7的執行原理概述

首先咱們補充說明下前文提到的PHP7程序執行過程,請參見圖3。

clipboard.png

圖3 PHP7語言編寫的程序的執行過程圖

第1步:詞法分析將PHP代碼轉換爲有意義的標識Token。該步驟的詞法分析器使用Re2c實現的。

第2步:語法分析將Token和符合文法規則的代碼生成抽象語法樹。語法分析器基於Bison實現。語法分析使用了巴科斯範式(BNF)來表達文法規則,Bison藉助狀態機、狀態轉移表和壓棧、出棧等一系列操做,生成抽象語法樹。

第3步:上步的抽象語法樹生成對應的opcode,被虛擬機執行。opcode是PHP7定義的一組指令標識,指令對應着相應的handler(處理函數)。當虛擬機調用opcode,會找到opcode背後的處理函數,執行真正的處理。以咱們常見的echo語句爲例,其對應的opcode即是ZEND_ECHO。

  • 注意:這裏爲了便於理解詞法分析和語法分析過程,將二者分開描述。但實際狀況,出於效率考慮,兩個過程並不是徹底獨立。

下面,咱們經過一段示例代碼,來創建PHP7運轉的初步理解。

示例代碼以下:

<?php
echo "hello world";

從圖3可知,這段代碼首先會被切割爲Token。

1. Token

Token是PHP代碼被切割成的有意義的標識。本書介紹的PHP7版本中有137 種Token,在zend_language_parser.h文件中作了定義:

/* Tokens.  */
#define END 0
#define T_INCLUDE 258
#define T_INCLUDE_ONCE 259
…
#define T_ERROR 392

更多Token的含義,感興趣的讀者能夠參考《PHP 7底層設計與源碼實現》附錄。

PHP提供了token_get_all()函數來獲取PHP代碼被切割後的Token,能夠在深刻源碼學習前,粗略查看PHP代碼被切割後的Token。以下代碼片斷:

/home/vagrant/php7/bin/php –r 'print_r(Token_get_all("<?php echo \"hello world\";"));'

輸出結果爲:

Array
(
   [0] => Array
       (
           [0] => 379
           [1] => <?php
           [2] => 1
       )
   [1] => Array
       (
           [0] => 328
           [1] => echo
           [2] => 1
       )
   [2] => Array
       (
           [0] => 382
           [1] =>
           [2] => 1
       )
   [3] => Array
       (
           [0] => 323
           [1] => "hello world"
           [2] => 1
       )
   [4] => ;
)

上文輸出中,二維數組的每一個成員數組第一個值爲Token對應的枚舉值;第二個值爲Token對應的原始字符串內容;第三個值爲代碼對應的行號。能夠看出,詞法解析器將 <?php echo "hello world"; 這段文本內容切分紅了4部分。

1)文本「<?php」,切割後對應的Token值爲379,參考PHP7中的源碼:

#dfine T_OPEN_TAG 379

不難理解,它是PHP代碼的起始tag,也就是<?php標識;

2)echo對應的Token是T_ECHO:

#define T_ECHO 328

3)源碼中的空格,對應的Token叫T_WHITESPACE,值爲382:

#define T_WHITESPACE 382

4)字符串「hello world」對應的Token值爲323:

#define T_CONSTANT_ENCAPSED_STRING 323

可見,Token就是一個個的「詞塊」,可是單獨存在的詞塊不能表達完整的語義,還須要藉助規則進行組織串聯。語法分析器就是這個組織者。它會檢查語法、匹配Token,對Token進行關聯。

PHP7中,組織串聯的產物就是抽象語法樹(Abstract Syntax Tree,AST)。

2. AST

AST是PHP7版本新特性。在這以前的版本,PHP代碼的執行過程當中沒有生成AST這一步。PHP7對抽象語法樹的支持,實現了PHP編譯器和解釋器解耦,有效提高了可維護性。

顧名思義,抽象語法樹具備樹狀結構。AST的節點分爲多種類型,對應着不一樣的PHP語法。在當前章節,咱們能夠認爲節點類型是對語法規則的抽象,例如賦值語句,生成的抽象語法樹節點爲ZEND_AST_ASSIGN。而賦值語句的左右操做數,又將做爲ZEND_AST_ASSIGN類型節點的孩子。經過這樣的節點關係,構建出抽象語法樹。

若是讀者但願一睹爲快,能夠直接跳到本書第13章函數的實現,其中圖片描繪了一段簡單的PHP代碼生成的抽象語法樹。

在這裏,咱們推薦讀者瞭解下PhpParser工具,能夠用它來查看PHP代碼生成的AST。

注意:PHP-Parser是PHP7內核做者之一nikic編寫的將PHP源碼生成AST的工具。源碼見https://github.com/nikic/PHP-...

3. Opcodes

AST扮演了源碼到中間代碼的臨時存儲介質的角色,還須要將其轉換爲opcode,才能被引擎直接執行。Opcode只是單條指令,Opcodes是opcode的集合形式,是PHP執行過程當中的中間代碼,相似Java中的字節碼。生成以後由虛擬機執行。

咱們知道,PHP工程優化措施中有個比較常見的「開啓Opcache」,指的就是這裏的Opcodes的緩存(Opcodes Cache)。經過省去從源碼到opcode的階段,引擎能夠直接執行緩存的opcode,以此提高性能。

藉助vld插件,能夠直觀地看到一段PHP代碼生成的opcode:

php -dvld.active=1 hello.php
通過過濾整理,對應的opcode爲:
line     op              
 1      ECHO            
 2      RETURN

其實在源碼實現中,上述代碼生成的opcode及handler爲:

ZEND_ECHO  // handler: ZEND_ECHO_SPEC_CONST_HANDLER
ZEND_RETURN  // handler: ZEND_RETURN_SPEC_CONST_HANDLER

可見,ZEND_ECHO對應的handler是ZEND_ECHO_SPEC_CONST_HANDLER。此handler的實現的功能即是預期的「hello world」語句的輸出。

本書的PHP版本中,內核在zend_vm_opcodes.h中定義了186種Opcodes,也能夠參考《PHP 7底層設計與源碼實現》附錄部分。

在平時的業務開發中,瞭解一些 PHP的底層實現,尤爲是語法機制的實現,對性能提高、故障排除很是有好處。但願這篇淺文,能夠幫助讀者在使用PHP7的同時,瞭解到編寫的PHP代碼如何被編譯和執行。

clipboard.png

clipboard.png

(掃上方二維碼7.9折購買)

推薦理由:

滴滴出行專家聯合撰寫,PHP領域大咖夏緒宏、韓天峯、王晶、謝華亮(黑夜路人)、伍星聯袂推薦

全面吃透PHP內核架構、核心實現與內存管理、詞法與句法解析、Zend 虛擬機、函數及關鍵擴展等設計細節與源碼實現

相關文章
相關標籤/搜索