【PHP7源碼學習】2019-04-08 PHP中include的實現

baiyanphp

所有視頻:https://segmentfault.com/a/11...前端

回顧while語法的實現

<?php
$a = 1;
while($a){
}
  • 在上一篇筆記中咱們知道,PHP中的while語法所對應的指令執行過程以下圖所示:

enter description here

  • 那麼如今回答一下上一篇文章結尾提出的問題:do-while是如何實現的呢?
<?php
$a = 1;
do{
    $a = 0;
}while($a);
  • 通過gdb調試,其最終的指令以下:

  • 第一個ASSIGN對應$a = 1;
  • 第二個ASSIGN對應$a = 0;
  • 第三個JMPNZ對應do-while循環體,注意這個箭頭指向$a = 0對應的ASSIGN指令,表明每次循環都要從新執行一次$a = -這個ASSIGN指令
  • 第四個RETURN對應PHP虛擬機自動給腳本添加的返回值

include語法的實現

  • 咱們在面試中常常會被問到以下知識點:nginx

    - include和require有什麼區別?
    - include和include_once有什麼區別(require和require_once)同理)
  • 以上兩道題的答案相信你們都知道,第一個問題若是文件不存在,include會狀況下會發出警告而require會報fatal error並終止腳本運行;而第二個問題中的include_once帶有緩存,若是以前加載過這個文件直接調用緩存中的文件,不會去二次加載文件,include_once的性能更好。
  • 咱們首先看一個例子:
  • 1.php:
<?php
$a = 1;
  • 2.php:
<?php
include "1.php";
$b = 2;
  • 那麼咱們經過gdb 2.php並分析它的op_array,能夠得出它的指令,一共有3條:
  • ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER:表示include "1.php";語句
  • ZEND_ASSIGN_SPEC_CV_CONST_RETVAL_UNUSED_HANDLER:表示$b = 2;
  • ZEND_RETURN_SPEC_CONST_HANDLER:表示PHP虛擬機自動給腳本加的返回值
  • 接下來咱們深刻第一個include的handler處理函數:
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_INCLUDE_OR_EVAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_op_array *new_op_array;

    zval *inc_filename;

    SAVE_OPLINE();
    inc_filename = EX_CONSTANT(opline->op1); //這裏的op1就是字符串1.php
    new_op_array = zend_include_or_eval(inc_filename, opline->extended_value);
    ...
  • 這個handler處理函數中核心爲zend_include_or_eval()這個函數,它返回一個新的op_array:
static zend_never_inline zend_op_array* ZEND_FASTCALL zend_include_or_eval(zval *inc_filename, int type) /* {{{ */
{
    zend_op_array *new_op_array = NULL;
    zval tmp_inc_filename;

    ...
    } else {
        switch (type) {
{
            case ZEND_INCLUDE_ONCE:
            case ZEND_REQUIRE_ONCE: { //此處帶有緩存
                    zend_file_handle file_handle;
                    zend_string *resolved_path;

                    resolved_path = zend_resolve_path(Z_STRVAL_P(inc_filename), (int)Z_STRLEN_P(inc_filename));
                    if (resolved_path) {
                        if (zend_hash_exists(&EG(included_files), resolved_path)) {
                            goto already_compiled;
                        }
                    } else {
                        resolved_path = zend_string_copy(Z_STR_P(inc_filename));
                    }

                    if (SUCCESS == zend_stream_open(ZSTR_VAL(resolved_path), &file_handle)) {

                        if (!file_handle.opened_path) {
                            file_handle.opened_path = zend_string_copy(resolved_path);
                        }

                        if (zend_hash_add_empty_element(&EG(included_files), file_handle.opened_path)) { //加入緩存的哈希表中
                            zend_op_array *op_array = zend_compile_file(&file_handle, (type==ZEND_INCLUDE_ONCE?ZEND_INCLUDE:ZEND_REQUIRE));
                            zend_destroy_file_handle(&file_handle);
                            zend_string_release(resolved_path);
                            if (Z_TYPE(tmp_inc_filename) != IS_UNDEF) {
                                zend_string_release(Z_STR(tmp_inc_filename));
                            }
                            return op_array;
                        } else {
                            zend_file_handle_dtor(&file_handle);
already_compiled:
                            new_op_array = ZEND_FAKE_OP_ARRAY;
                        }
                    } else {
                        if (type == ZEND_INCLUDE_ONCE) {
                            zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, Z_STRVAL_P(inc_filename));
                        } else {
                            zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, Z_STRVAL_P(inc_filename));
                        }
                    }
                    zend_string_release(resolved_path);
                }
                break;
            case ZEND_INCLUDE:
            case ZEND_REQUIRE:
                new_op_array = compile_filename(type, inc_filename);  //關鍵調用
                break;
    ...
    return new_op_array;
}
  • 在ZEND_INCLUDE_ONCE分支中能夠觀察到,若是是include_once或者require_once的case,會先去緩存中查找,那麼這個緩存是怎麼實現的呢?最容易想到的就是哈希表,key爲文件名,value爲文件內容,這樣就能夠直接從緩存中讀取文件,不用再次加載文件了,提升效率。
  • 咱們回到主題include語法,在ZEND_INCLUDE分支中咱們能夠看到,這裏又調用了一個新的函數compile_filename(),它返回一個新的op_array。由於include是包含另外一個外部文件,而op_array是一個腳本的指令集,因此須要新建立一個op_array,存儲另一個文件的指令集,咱們繼續跟進compile_filename():
zend_op_array *compile_filename(int type, zval *filename)
{
    zend_file_handle file_handle;
    zval tmp;
    zend_op_array *retval;
    zend_string *opened_path = NULL;
    
    ...
    retval = zend_compile_file(&file_handle, type); //核心調用
    
    return retval;
}
  • 這個函數中還會繼續調用zend_compile_file()函數,它是一個函數指針,指向compile_file()函數:
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type)
{
    ...
    zend_op_array *op_array = NULL;

    if (open_file_for_scanning(file_handle)==FAILURE) {
        ...
    } else {
        op_array = zend_compile(ZEND_USER_FUNCTION); //核心調用
    }

    return op_array;
}
  • 咱們能夠看到,它最終調用了zend_compile函數。咱們是否是對它很熟悉呢?沒錯,它就是PHP腳本編譯的入口。隨後,經過調用這個函數,就能夠對引入的外部腳本1.php進行詞法分析和語法分析等編譯操做了。
  • 如今思考一個問題,這個函數返回一個op_array,是引入的新的外部腳本1.php的op_array,那麼原來的舊腳本2.php的op_array的狀態和數據應該如何存儲呢?
  • op_array是存儲在zend_execute_data棧上的,那麼新的腳本1.php的op_array就能夠繼續往zend_execute_data棧中添加。當include腳本執行完成以後,出棧便可。同遞歸的原理同樣,遞歸也是藉助棧,當你不斷遞歸的時候,數據不斷入棧,到最後的遞歸終止條件的時候,逐步出棧便可,因此遞歸是很是慢的,效率極低。

其餘

PHP腳本的執行流程

  • 咱們以前講過,PHP腳本的執行入口爲main函數(咱們代碼層面沒法看到,是虛擬機幫助咱們加的)。從main函數進去以後,PHP腳本的執行總共有5大階段:
  • CLI模式(command line interface,即命令行模式。如在命令行下執行腳本:php 1.php):
  • php_module_startup:模塊初始化
  • php_request_startup:請求初始化
  • php_execute_script:執行腳本
  • php_request_shutdown:請求關閉
  • php_module_shutdown:模塊關閉
  • CLI模式下,運行一次就會直接退出,並不常駐內存,接下來看一下咱們使用的最多的FPM模式,它常駐內存。一次請求到來,PHP-FPM就要對其進行處理,因此在 php_request_startup、php_execute_script、php_request_shutdown三個階段會進行死循環,讓PHP-FPM常駐內存,才能不斷地處理一個個到來的請求。可是這樣會有一個問題,每個請求到來的時候,都會從新進行詞法解析、語法解析......效率是很是低的。爲了解決這個問題,PHP中咱們常說的opcache就要粉墨登場了。它會把以前解析過的opcode緩存起來,下一次再遇到相同opcode的時候,就不用再次解析,提高性能。

初探nginx+php-fpm架構

  • 在LNMP架構下,前端的請求發來,先會經過nginx作代理,而後經過fastcgi協議,轉發給上游的php-fpm,由php-fpm真正地處理請求。
  • 咱們知道,nginx是多進程架構的反向代理web服務器,由一個master進程和多個worker進程組成:
  • master進程:管理全部worker進程(如worker進程的建立、銷燬)
  • worker進程:負責處理客戶端發來的請求
  • 當殺死master進程的時候,worker進程依然存在,能夠爲客戶端提供服務
  • 當殺死worker進程的時候(且當前沒有其餘worker進程),master進程就會再建立worker進程,保證nginx服務正常運行
  • 下一篇文章咱們就即將講解fastcgi協議,逐步揭開nginx+php-fpm架構通訊的神祕面紗
相關文章
相關標籤/搜索