官术网_书友最值得收藏!

2.1 PHP 7語言的執行原理

我們常用的高級語言有很多種,比較出名的有C\C++、Python、PHP、Go、Pascal等。而這些語言根據運行的方式不同,大體分為兩種:編譯型語言和解釋型語言

其中,編譯型語言包括C\C++、Pascal、Go等。這里說的編譯是指在應用源程序執行之前,就將程序源代碼“翻譯”成匯編語言,然后進一步根據軟硬件環境編譯成目標文件。一般稱完成編譯工作的工具為編譯器。而解釋型語言,在程序運行時才被“翻譯”為機器語言。但是執行一次“翻譯”一次,所以執行效率較低。解釋器的工作就是解釋型語言中,負責“翻譯”源代碼的程序。

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

2.1.1 編譯型語言與解釋型語言

我們知道,對于一段C語言代碼,需要經過預編譯、編譯、匯編和鏈接,才能成為可執行的二進制文件。以hello.c為例:

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

圖2-1 編譯型語言的執行示意

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

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

第2步:編譯。編譯器會把C語言翻譯成匯編語言程序,一條C語言通常編譯為多條匯編代碼。同時編譯器會對程序進行優化,生成目標匯編程序。

第3步:編譯得到的匯編語言通過匯編器再匯編成目標程序hello.o。

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

以C語言為代表的編譯型語言,代碼發生更新都要經過以上步驟。

我們在本章對編譯型語言與解釋型語言的區別的理解,立足于源代碼被編譯成目標平臺CPU指令的時機。對于編譯型語言,編譯結果已經是針對當前CPU體系的指令;而解釋型語言,需要先編譯成中間代碼,再經由該解釋型語言的特定虛擬機,翻譯成特定CPU體系的指令被執行。解釋型語言是在運行過程中,翻譯為目標平臺的指令。常說解釋型語言“慢”,主要也是慢在這里。

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

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

圖2-2 以PHP為例,解釋型語言的執行示意

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

第1步:源碼通過詞法分析得到Token。

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

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

接下來在基本步驟的基礎上,細化PHP語言的執行原理,以便更清晰地建立認知。

2.1.2 PHP 7的執行原理概述

圖2-3 PHP 7程序的執行過程

首先補充說明前文提到的PHP 7程序執行過程,請見圖2-3。

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

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

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

注意

這里為了便于理解詞法分析和語法分析過程,將兩者分開描述。但實際情況下,出于效率考慮,兩個過程并非完全獨立。

下面通過一段示例代碼,來建立PHP 7運轉的初步理解。

示例代碼如下:

    <? php
    echo "hello world";

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

1. Token

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

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

更多Token的含義,感興趣的讀者可以參考附錄B。

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,參考PHP 7中的源碼:

    #define 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進行關聯。

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

2. AST

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

顧名思義,抽象語法樹具有樹狀結構。AST的節點分為多種類型,對應著PHP語法。在當前章節,我們可以認為節點類型是對語法規則的抽象,例如賦值語句,生成的抽象語法樹節點為ZEND_AST_ASSIGN。而賦值語句的左右操作數又將作為ZEND_AST_ASSIGN類型節點的孩子。通過這樣的節點關系,構建出抽象語法樹。

如果讀者希望一睹為快,可以直接跳到本書第13章,其中圖片描繪了一段簡單的PHP代碼生成的抽象語法樹。

這里介紹PHP-Parser工具,它可以用來查看PHP代碼生成的AST。

注意

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

更多關于抽象語法樹的介紹,將在后續章節展開。

3. opcodes

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

我們知道,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,也可以參考本書附錄B。

主站蜘蛛池模板: 延川县| 皮山县| 望奎县| 伊川县| 延津县| 阿图什市| 五莲县| 阜宁县| 仁布县| 阿荣旗| 镇雄县| 临高县| 宁都县| 卫辉市| 洛扎县| 同心县| 牙克石市| 浦城县| 贞丰县| 南宫市| 革吉县| 临汾市| 普兰店市| 柳州市| 遂溪县| 湖南省| 云林县| 延长县| 榆中县| 潼关县| 渝中区| 平遥县| 资讯 | 光山县| 嵊泗县| 来宾市| 九台市| 澳门| 托克逊县| 瓮安县| 鹤山市|