PHP解釋器引擎執行流程 - [ PHP內核學習 ]

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信息的附加功能,因爲引擎初始化是發生在模塊請求初始化之前,而模塊請求初始化又是在編譯和執行之前,所以這樣的覆蓋能達到目的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章