PHP直譯器引擎執行流程 - [ PHP核心學習 ]

Andrew.Hann發表於2016-01-29

catalogue

1. SAPI介面
2. PHP CLI模式解釋執行指令碼流程
3. PHP Zend Complile/Execute函式介面化(Hook Call架構基礎)

 

1. SAPI介面

PHP的SAPI層實現上層介面的封裝,使得PHP可以用在很多種模式場景下(例如apache、ningx、cgi、fastcgi、cli),以以cli SAPI為例子學習PHP直譯器引擎是如何處理PHP使用者態原始碼檔案的
Cli(Command Line Interface)即PHP的命令列模式,現在此SAPI是預設安裝的,我們在伺服器上安裝完PHP之後,一般會生成一個可執行檔案

指令碼執行的開始都是以SAPI介面實現開始的。只是不同的SAPI介面實現會完成他們特定的工作, 例如Apache的mod_php SAPI實現需要初始化從Apache獲取的一些資訊,在輸出內容是將內容返回給Apache, 其他的SAPI實現也類似

0x1: sapi_module_struct

要定義個SAPI,首先要定義個sapi_module_struct
PHP-SRC/sapi/cli/php_cli.c

/* {{{ sapi_module_struct cli_sapi_module
 */
static sapi_module_struct cli_sapi_module = {
    "cli",                            /* name php_info()的時候被使用 */
    "Command Line Interface",        /* pretty name */

    php_cli_startup,                /* startup */
    php_module_shutdown_wrapper,    /* shutdown */

    NULL,                            /* activate */
    sapi_cli_deactivate,            /* deactivate */

    sapi_cli_ub_write,                /* unbuffered write */
    sapi_cli_flush,                    /* flush */
    NULL,                            /* get uid */
    NULL,                            /* getenv */

    php_error,                        /* error handler */

    sapi_cli_header_handler,        /* header handler */
    sapi_cli_send_headers,            /* send headers handler */
    sapi_cli_send_header,            /* send header handler */

    NULL,                            /* read POST data */
    sapi_cli_read_cookies,          /* read Cookies */

    sapi_cli_register_variables,    /* register server variables */
    sapi_cli_log_message,            /* Log message */
    NULL,                            /* Get request time */
    NULL,                            /* Child terminate */
    
    STANDARD_SAPI_MODULE_PROPERTIES
};
/* }}} */

這個結構,包含了一些常量,比如name, 這個會在我們呼叫php_info()的時候被使用。一些初始化,收尾函式,以及一些函式指標,用來告訴Zend,如何獲取,和輸出資料,我們在下面的流程介紹中就會逐個涉及到其中的欄位

Relevant Link:

http://www.nowamagic.net/librarys/veda/detail/1285

 

2. PHP CLI模式解釋執行指令碼流程

0x1: Process Startup

主程式main在進行一些必要的初始化工作後,就進入SAPI的邏輯流程,初始化的一些環境變數,這將在整個SAPI生命週期中發生作用

0x2: MINIT

進入特定的SAPI模式之後,PHP呼叫各個擴充套件的MINIT方法
\php-5.6.17\sapi\cli\php_cli.c

int main(int argc, char *argv[])
{
    ..
    sapi_module_struct *sapi_module = &cli_sapi_module;
    ..
    sapi_module->ini_defaults = sapi_cli_ini_defaults;
    sapi_module->php_ini_path_override = ini_path_override;
    sapi_module->phpinfo_as_text = 1;
    sapi_module->php_ini_ignore_cwd = 1;
    sapi_startup(sapi_module);
    sapi_started = 1;
    ..

php_cli_startup

static int php_cli_startup(sapi_module_struct *sapi_module) /* {{{ */
{
    if (php_module_startup(sapi_module, NULL, 0)==FAILURE) {
        return FAILURE;
    }
    return SUCCESS;
}

PHP呼叫各個擴充套件的MINIT方法,從而使這些擴充套件切換到可用狀態

/* {{{ php_module_startup
 */
int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
    ..
    zend_module_entry *module;
    ..
    module_shutdown = 0;
    module_startup = 1;
    sapi_initialize_empty_request(TSRMLS_C);
    sapi_activate(TSRMLS_C);
    ..
    /* start additional PHP extensions */
    php_register_extensions_bc(additional_modules, num_additional_modules TSRMLS_CC);

    /* load and startup extensions compiled as shared objects (aka DLLs)
       as requested by php.ini entries
       theese are loaded after initialization of internal extensions
       as extensions *might* rely on things from ext/standard
       which is always an internal extension and to be initialized
       ahead of all other internals
     */
    php_ini_register_extensions(TSRMLS_C);
    zend_startup_modules(TSRMLS_C);

    /* start Zend extensions */
    zend_startup_extensions();
    ..

MINIT的意思是"模組初始化"。各個模組都定義了一組函式、類庫等用以處理其他請求
一個典型的MINIT方法如下

PHP_MINIT_FUNCTION(extension_name){ /* Initialize functions, classes etc */ }

0x3: RINIT

當一個頁面請求發生時,SAPI層將控制權交給PHP層。於是PHP設定了用於回覆本次請求所需的環境變數。同時,它還建立一個變數表,用來存放執行過程 中產生的變數名和值。PHP呼叫各個模組的RINIT方法,即"請求初始化"
一個經典的例子是Session模組的RINIT,如果在php.ini中 啟用了Session模組,那在呼叫該模組的RINIT時就會初始化$_SESSION變數,並將相關內容讀入
RINIT方法可以看作是一個準備過程, 在程式執行之前就會自動啟動。一個典型的RINIT方法如下

PHP_RINIT_FUNCTION(extension_name) { /* Initialize session variables,pre-populate variables, redefine global variables etc */ }

PHP會在每個request的時候,處理一些初始化,資源分配的事務。這部分就是activate欄位要定義的,從上面的結構我們可以看出,從上面cli對應的cli_sapi_module結構體來看,對於CGI來說,它並沒有提供初始化處理控制程式碼。對於mod_php來說,那就不同了,他要在apache的pool中註冊資源解構函式,申請空間, 初始化環境變數,等等

0x4: SCRIPT

PHP通過php_execute_script(&file_handle TSRMLS_CC)來執行PHP的指令碼
\php-5.6.17\main\main.c

/* {{{ php_execute_script
 */
PHPAPI int php_execute_script(zend_file_handle *primary_file TSRMLS_DC)
{
    //file_handle的型別為zend_file_handle,這個是zend對檔案控制程式碼的一個封裝,裡面的內容和待執行指令碼相關
    zend_file_handle *prepend_file_p, *append_file_p;
    zend_file_handle prepend_file = {0}, append_file = {0};
    ..
    //php_execute_script最終是呼叫的zend_execute_scripts
    retval = (zend_execute_scripts(ZEND_REQUIRE TSRMLS_CC, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS);
    ..

php_execute_script最終是呼叫的zend_execute_scripts
{PHPSRC}/Zend/zend.c

//此函式具有可變引數,可以一次執行多個PHP檔案
ZEND_API int zend_execute_scripts(int type TSRMLS_DC, zval **retval, int file_count, ...) /* {{{ */
{
    ..
    EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC);
    ..
    if (EG(active_op_array)) 
    {
        EG(return_value_ptr_ptr) = retval ? retval : NULL;
        zend_execute(EG(active_op_array) TSRMLS_CC);
        ..

1. compile編譯過程

zend_compile_file是一個函式指標,其宣告在{PHPSRC}/Zend/zend_compile.c中

ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type TSRMLS_DC);  

在引擎初始化的時候,會將compile_file函式的地址賦值給zend_compile_file,compile_file函式定義在{PHPSRC}/Zend/zend_language_scanner.l

//函式以zend_file_handle指標作為引數,返回一個指向zend_op_array的指標
ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type TSRMLS_DC)
{
    ..
    //Lex詞法解析過程
    ..

2. execute執行過程(逐條執行opcode)

zend_execute也是一個函式指標(利用compile過程得到的opcode array),其宣告在{PHPSRC}/Zend/zend_execute.c

ZEND_API extern void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);  

在引擎初始化的時候,會將execute函式的地址賦值給zend_execute,execute的定義在{PHPSRC}/Zend/zend_vm_execute.h

//zend_execute以一個指向zend_op_array結構的指標作為引數,這個指標即前面zend_compile_file的返回值,zend_execute就開始執行op_array中的op code,在執行op code的過程中,就實現了PHP語言的各種功能
ZEND_API void zend_execute(zend_op_array *op_array TSRMLS_DC)
{
    if (EG(exception)) {
        return;
    } 
    zend_execute_ex(i_create_execute_data_from_op_array(op_array, 0 TSRMLS_CC) TSRMLS_CC);
}

0x5: RSHUTDOWN

一旦頁面執行完畢(無論是執行到了檔案末尾還是用exit或die函式中止),PHP就會啟動清理程式。它會按順序呼叫各個模組的RSHUTDOWN方法。 RSHUTDOWN用以清除程式執行時產生的符號表,也就是對每個變數呼叫unset函式

PHP_RSHUTDOWN_FUNCTION(extension_name) { /* Do memory management, unset all variables used in the last PHP call etc */ }

0x6: MSHUTDOWN

最後,所有的請求都已處理完畢,SAPI也準備關閉了,PHP開始執行第二步:PHP呼叫每個擴充套件的MSHUTDOWN方法,這是各個模組最後一次釋放記憶體的機會

PHP_MSHUTDOWN_FUNCTION(extension_name) { /* Free handlers and persistent memory etc */ }

/main/main.c

/* {{{ php_module_shutdown_wrapper
 */
int php_module_shutdown_wrapper(sapi_module_struct *sapi_globals)
{
    TSRMLS_FETCH();
    php_module_shutdown(TSRMLS_C);
    return SUCCESS;
}

Relevant Link:

http://www.nowamagic.net/librarys/veda/detail/1286
http://www.nowamagic.net/librarys/veda/detail/1322
http://www.nowamagic.net/librarys/veda/detail/1323
http://www.nowamagic.net/librarys/veda/detail/1332
http://blog.csdn.net/phpkernel/article/details/5716342
http://www.nowamagic.net/librarys/veda/detail/1287
http://www.nowamagic.net/librarys/veda/detail/1289

 

3. PHP Zend Complile/Execute函式介面化(Hook Call架構基礎)

PHP核心在設計架構實現的時候,除了提供了擴充套件機制,還在Zend的兩個關鍵流程(compile、execute)提供了Hook機制,PHP擴充套件開發人員可以Hook劫持Zend的編譯/解釋執行流程,在Zend編譯執行之前先執行自定義的程式碼邏輯,然後再交還控制權給Zend。在引擎初始化(zend_startup)的時候

1. end_execute指向了預設的execute
2. zend_compile_file指向了預設的compile_file

我們可以在實際編譯和執行之前(RINIT階段中)將zend_execute和zend_compile_file重寫為其他的編譯和執行函式,這樣就為我們擴充套件引擎留下了鉤子,比如一個比較有名的檢視PHP的op code的擴充套件vld,此擴充套件就是在每次請求初始化的鉤子函式(PHP_RINIT_FUNCTION)中,將zend_execute和zend_compile_file替換成自己的vld_execute和vld_compile_file,這兩個函式其實是對原始函式進行了封裝,新增了輸出opcode資訊的附加功能,因為引擎初始化是發生在模組請求初始化之前,而模組請求初始化又是在編譯和執行之前,所以這樣的覆蓋能達到目的

Relevant Link:

Copyright (c) 2016 LittleHann All rights reserved

 

相關文章