[翻譯][php擴展開發和嵌入式]第1章-php的生命週期

全部翻譯內容pdf文檔下載地址: http://download.csdn.net/detail/lgg201/5107012

本書目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)兩位大牛組織翻譯. 該翻譯項目地址爲: https://github.com/walu/phpbook

原書名: <Extending and Embedding PHP>

原作者: Sara Golemon

譯者: goosman.lei(雷果國)

譯者Email: [email protected]

譯者Blog: http://blog.csdn.net/lgg201


php的生命週期

在常見的webserver環境中, 你不能直接啓動php解釋器; 一般是啓動apache或其他webserver, 由它們加載php處理需要處理的腳本(請求的.php文檔).

一切都從sapi開始

儘管看起來有所不同, 但實際上CLI的行爲和web方式一致. 在命令行中鍵入php命令將啓動"命令行sapi", 它實際上就像一個設計用於服務單請求的迷你版webserver. 當腳本運行完成後, 這個迷你的php-webserver終止並返回控制給shell.

啓動和終止

這裏的啓動和終止過程分爲兩個獨立的啓動階段和兩個獨立的終止階段. 一個週期用於php解釋器整體執行所需結構和值的初始化設置, 它們在sapi生命週期中持久存在. 另一個則僅服務於單頁面請求, 生命週期短暫一些.

初始化啓動在所有的請求發生之前, php調用每個擴展的MINIT(模塊初始化)方法. 這裏, 擴展可能會定義常量, 定義類, 註冊資源, 流, 過濾處理器等所有將要被請求腳本所使用的資源. 所有這些都有一個特性, 就是它們被設計跨所有請求存在, 也可以稱爲"持久".

常見的MINIT方法如下:

/* 初始化myextension模塊
 * 這在sapi啓動後將立即發生
 */
PHP_MINIT_FUNCTION(myextension)
{
    /* 全局: 第12章 */

#ifdef ZTS
    ts_allocate_id(&myextension_globals_id,
        sizeof(php_myextension_globals),
        (ts_allocate_ctor) myextension_globals_ctor,
        (ts_allocate_dtor) myextension_globals_dtor);
#else
    myextension_globals_ctor(&myextension_globals TSRMLS_CC);
#endif


    /* REGISTER_INI_ENTRIES() 指向一個全局的結構, 我們將在第13章"INI設置"中學習 */
    REGISTER_INI_ENTRIES();


    /* 等價於define('MYEXT_MEANING', 42); */
    REGISTER_LONG_CONSTANT("MYEXT_MEANING", 42, CONST_CS | CONST_PERSISTENT);
    /* 等價於define('MYEXT_FOO', 'bar'); */
    REGISTER_STRING_CONSTANT("MYEXT_FOO", "bar", CONST_CS | CONST_PERSISTENT);


    /* 資源: 第9章 */
    le_myresource = zend_register_list_destructors_ex(
                    php_myext_myresource_dtor, NULL,
                    "My Resource Type", module_number);
    le_myresource_persist = zend_register_list_destructors_ex(
                    NULL, php_myext_myresource_dtor,
                    "My Resource Type", module_number);


    /* 流過濾器: 第16章 */
    if (FAILURE == php_stream_filter_register_factory("myfilter",
                   &php_myextension_filter_factory TSRMLS_CC)) {
        return FAILURE;
    }


    /* 流包裝器: 第15章 */
    if (FAILURE == php_register_url_stream_wrapper ("myproto",
                   &php_myextension_stream_wrapper TSRMLS_CC)) {
        return FAILURE;
    }


    /* 自動全局變量: 第12章 */
#ifdef ZEND_ENGINE_2
    if (zend_register_auto_global("_MYEXTENSION", sizeof("_MYEXTENSION") - 1,
                                                NULL TSRMLS_CC) == FAILURE) {
        return FAILURE;
    }
    zend_auto_global_disable_jit ("_MYEXTENSION", sizeof("_MYEXTENSION") - 1
                                                     TSRMLS_CC);
#else
    if (zend_register_auto_global("_MYEXTENSION", sizeof("_MYEXTENSION") - 1
                                                     TSRMLS_CC) == FAILURE) {
        return FAILURE;
    }
#endif
    return SUCCESS;
}

在一個請求到達時, php會安裝一個操作環境, 該環境包含符號表(變量存儲), 並且會同步每個目錄的配置值. php接着遍歷所有的擴展, 這一次調用每個擴展的RINIT(請求初始化)方法. 這裏, 擴展可能充值全局變量到默認值, 預置變量到腳本的符號表, 或執行其他的任務比如記錄頁面請求日誌到文件. RINIT之於所有腳本請求就像auto_prepend_file指令一樣.

RINIT方法的寫法如下:

/* 每個頁面請求開始之前執行
 */
PHP_RINIT_FUNCTION(myextension)
{
    zval *myext_autoglobal;


    /* 初始化MINIT函數中定義的自動全局變量爲空數組. 這等價於$_MYEXTENSION = array(); */
    ALLOC_INIT_ZVAL(myext_autoglobal);
    array_init(myext_autoglobal);
    zend_hash_add(&EG(symbol_table), "_MYEXTENSION", sizeof("_MYEXTENSION") - 1,
                                (void**)&myext_autoglobal, sizeof(zval*), NULL);


    return SUCCESS;
}

在一個請求完成處理後(到達腳本文件末尾或調用了die()/exit()語句), php通過調用每個擴展的RSHUTDOWN(請求終止)開始清理過程. 就像RINIT對應於auto_prepend_file, RSHUTDOWN可以類比auto_append_file指令. 在RSHUTDOWN和auto_append_file之間, 最重要的不同是: 無論如何, RSHUTDOWN總會被執行, 而用戶空間腳本的die()/exit()調用會跳過所有的auto_append_file.

在符號表和其他資源釋放之前所需要做的最後一件事舊是RSHUTDOWN. 在所有的RSHUTDOWN方法完成後, 符號表中的所有變量都會被立即unset(), 

在此期間, 所有非持久化資源和對象的析構器都將被調用去優雅的釋放資源.

/* 每個頁面請求結束後調用 */
PHP_RSHUTDOWN_FUNCTION(myextension)
{
    zval **myext_autoglobal;


    if (zend_hash_find(&EG(symbol_table), "_MYEXTENSION", sizeof("_MYEXTENSION"),
                                         (void**)&myext_autoglobal) == SUCCESS) {
        /* 做一些對$_MYEXTENSION數組的值有意義的處理 */
        php_myextension_handle_values(myext_autoglobal TSRMLS_CC);
    }
    return SUCCESS;
}

最後, 當所有的請求都被滿足(完成處理)後, webserver或其他sapi就開始準備終止, php循環執行每個擴展的MSHUTDOWN(模塊終止)方法. 這是MINIT週期內, 擴展最後一次卸載處理器和釋放持久化分配的內存的機會.

/* 這個模塊正在被卸載, 常量和函數將被自動的卸載, 持久化資源, 類, 流處理器必須手動的卸載. */
PHP_MSHUTDOWN_FUNCTION(myextension)
{
    UNREGISTER_INI_ENTRIES();
    php_unregister_url_stream_wrapper ("myproto" TSRMLS_CC);
    php_stream_filter_unregister_factory ("myfilter" TSRMLS_CC);
    return SUCCESS;
}

生命週期

每個php實例, 無論從init腳本啓動還是從命令行啓動, 接下來都是上一節講到的一系列請求/模塊的初始化/終止事件, 以及腳本自身的執行. 每個啓動和終止階段會被執行多少次? 以什麼頻率執行? 都依賴於所使用的sapi. 下面討論4種最常見的sapi: cli/cgi, 多進程模塊, 多線程模塊, 嵌入式.

cli生命週期

cli(和cgi)sapi在它的單請求生命週期中相當特殊, 因爲此時整個php的生命週期只有一個請求. 不過, 麻雀雖小五臟俱全, 前面講的各個階段仍然會全部執行. 下圖是從命令行調用php解釋器處理test.php腳本的處理過程:


多進程生命週期

php最常見的用法是將php構建爲apache 1或使用pre-fork MPM的apache 2的apxs模塊, 將php嵌入到webserver中. 還有一些其他webserver也是屬於這一類的, 本書後面將它們統稱爲"多進程模塊".

將它們稱爲多進程模塊是因爲當apache啓動時, 它會立即fork出一些子進程, 每個都有自己的獨立的進程空間, 互相之間獨立. 在一個子進程中, php實例的生命週期就像下圖所示一樣. 這裏唯一的變化是多個請求被夾在在單個的MINIT/MSHUTDOWN對中:


這種模式不允許任意子進程知道其他子進程擁有的數據, 不過它允許子進程死亡並被替換, 而不影響其他子進程的穩定性. 下圖戰士了一個apache實例中的多個進程以及它們對MINIT, RINIT, RSHUTDOWN, MSHUTDOWN方法的調用.


多線程生命週期

隨着發展, php逐漸的被一些webserver以多線程方式使用, 比如IIS的isapi接口, apapche 2的worker mpm. 在多線程webserver中永遠都只有一個進程在運行, 但是在進程空間中有多個線程同時執行. 這樣做能降低一些負載, 包括避免了MINIT/MSHUTDOWN的重複調用, 真正的全局數據只被分配和初始化一次, 潛在的打開了多個請求的信息共享之門. 下圖展示了apache 2這樣的多線程webserver上運行php時的進程狀態:



嵌入式生命週期

回顧前面, 嵌入式sapi只是sapi的另外一種實現, 它還是遵循和cli, apxs, isapi接口一致的規則, 因此很容易猜到請求的生命週期遵循相同的基本路徑: 模塊初始化 => 請求初始化 => 請求 => 請求終止 => 模塊終止. 實際上, 嵌入式sapi和它的同族一樣遵循着這些步驟.

讓嵌入式sapi變得特殊的是它可能被當做一個整個請求的一部分被潛入到多個腳本片段中. 多數情況下控制會在php和調用應用之間多次來回的傳遞.

雖然一個嵌入請求可能由一個或多個代碼元素組成, 但嵌入式應用還是受和webserver一樣的請求隔離影響. 爲了處理兩個或多個並行的嵌入環境, 你的應用要麼像apache 1去fork, 要麼舊像apache 2線程化. 嘗試在單個非線程進程空間中處理兩個獨立的請求環境將產生不可預料的結果, 這肯定是你不期望的.

Zend線程安全

當php還在幼兒期的時候, 它作爲一個單進程cgi運行, 並沒有線程安全的概念, 因爲沒有比單個請求存活更久的進程空間. 內部變量可以在全局作用域中定義, 訪問, 修改, 只要初始化沒有問題就不會產生嚴重後果. 任何沒有被正確清理的資源都會在cgi進程終止時被釋放.

後來, php嵌入了多進程webserver, 比如apache. 給定的內部變量仍然可以定義在全局並且可以通過在每個請求啓動時正確的初始化, 終止時去做適當的清理工作來做到安全訪問, 因爲在一個進程空間中同時只會有一個請求. 這個時候, 增加了每個請求的內存管理, 以放置資源泄露的增長失去控制.

單進程多線程webserver出現後, 就需要一種對全局數據處理的新的方法. 最後這作爲新的一層TSRM(線程安全資源管理)

線程安全Vs.廢線程安全定義

在一個簡單的非線程應用中, 你可能很喜歡定義全局變量, 將它們放在你的源代碼的頂部. 編譯器會在你的程序的數據段分配內存塊保存信息.

在多線程應用中, 每個線程需要它自己的數據元素, 需要爲每個線程分配獨立的內存塊. 一個給定線程在它需要訪問自己的數據時需要能夠正確的訪問到自己的這個內存塊.

線程安全數據池

在一個擴展的MINIT階段, 擴展可以調用ts_allocate_id()一次或多次告訴TSRM層它需要多少數據空間, TSRM接收到通知後, 將總的運行數據空間增大請求的字節數, 並返回一個新的唯一的標識, 標記線程數據池的數據段部分.

typedef struct {
    int sampleint;
    char *samplestring;
} php_sample_globals;
int sample_globals_id;
PHP_MINIT_FUNCTION(sample)
{
    ts_allocate_id(&sample_globals_id,
        sizeof(php_sample_globals),
        (ts_allocate_ctor) php_sample_globals_ctor,
        (ts_allocate_dtor) php_sample_globals_dtor);
    return SUCCESS;
}

當一個請求需要訪問數據段的時候,擴展從TSRM層請求當前線程的資源池,以ts_allocate_id()返回的資源ID來獲取偏移量。

換句話說,在代碼流中,你可能會在前面所說的MINIT語句中碰到SAMPLE_G(sampleint) = 5;這樣的語句。在線程安全的構建下,這個語句通過一些宏擴展如下:

(((php_sample_globals*)(*((void ***)tsrm_ls))[sample_globals_id-1])->sampleint = 5;

如果你看不懂上面的轉換也不用沮喪,它已經很好的封裝在PHPAPI中了,以至於許多開發者都不需要知道它怎樣工作的。

當不在線程環境時

因爲在PHP的線程安全構建中訪問全局資源涉及到在線程數據池查找對應的偏移量,這是一些額外的負載,結果就是它比對應的非線程方式(直接從編譯期已經計算好的真實的全局變量地址中取出數據)慢一些。

考慮上面的例子,這一次在非線程構建下:

typedef struct {
    int sampleint;
    char *samplestring;
} php_sample_globals;
php_sample_globals sample_globals;
PHP_MINIT_FUNCTION(sample)
{
    php_sample_globals_ctor(&sample_globals TSRMLS_CC);
    return SUCCESS;
}

首先注意到的是這裏並沒有定義一個int型的標識去引用全局的結構定義,只是簡單的在進程的全局空間定義了一個結構體。也就是說SAMPLE_G(sampleint) = 5;展開後就是sample_globals.sampleint = 5; 簡單,快速,高效。

非線程構建還有進程隔離的優勢,這樣給定的請求碰到完全出乎意料的情況時,它也不會影響其他進程,即便是產生段錯誤也不會導致整個webserver癱瘓。實際上,Apache的MaxRequestsPerChild指令就是設計用來提升這個特性的,它經常性的有目的性的kill掉子進程併產生新的子進程,來避免某些可能由於進程長時間運行“累積”而來的問題(比如內存泄露)。

訪問全局變量

在創建一個擴展時,你並不知道它最終的運行環境是否是線程安全的。幸運的是,你要使用的標準包含文件集合中已經包含了條件定義的ZTS預處理標記。當PHP因爲SAPI需要或通過enable-maintainer-zts選項安裝等原因以線程安全方式構建時,這個值會被自動的定義,並可以用一組#ifdef ZTS這樣的指令集去測試它的值。

就像你前面看到的,只有在PHP以線程安全方式編譯時,纔會存在線程安全池,只有線程安全池存在時,纔會真的在線程安全池中分配空間。這就是爲什麼前面的例子包裹在ZTS檢查中的原因,非線程方式供非線程構建使用。

在本章前面PHP_MINIT_FUNCTION(myextension)的例子中,你可以看到#ifdef ZTS被用作條件調用正確的全局初始代碼。對於ZTS模式它使用ts_allocate_id()彈出myextension_globals_id變量,而非ZTS模式只是直接調用myextension_globals的初始化方法。這兩個變量已經在你的擴展源文件中使用Zend宏:DECLARE_MODULE_GLOBALS(myextension)聲明,它將自動的處理對ZTS的測試並依賴構建的ZTS模式選擇正確的方式聲明。

在訪問這些全局變量的時候,你需要使用前面給出的自定義宏SAMPLE_G()。在第12章,你將學習到怎樣設計這個宏以使它可以依賴ZTS模式自動展開。

即便你不需要線程也要考慮線程

正常的PHP構建默認是關閉線程安全的,只有在被構建的sapi明確需要線程安全或線程安全在./configure階段顯式的打開時,纔會以線程安全方式構建。

給出了全局查找的速度問題和進程隔離的缺點後,你可能會疑惑爲什麼明明不需要還有人故意打開它呢?這是因爲,多數情況下,擴展和SAPI的開發者認爲你是線程安全開關的操作者,這樣做可以很大程度上確保新代碼可以在所有環境中正常運行。

當線程安全啓用時,一個名爲tsrm_ls的特殊指針被增加到了很多的內部函數原型中。這個指針允許PHP區分不同線程的數據。回想一下本章前面ZTS模式下的SAMPLE_G()宏函數中就使用了它。沒有它,正在執行的函數就不知道查找和設置哪個線程的符號表;不知道應該執行哪個腳本,引擎也完全無法跟蹤它的內部寄存器。這個指針保留了線程處理的所有頁面請求。

這個可選的指針參數通過下面一組定義包含到原型中。當ZTS禁用時,這些定義都被展開爲空;當ZTS開啓時,它們展開如下:

#define TSRMLS_D     void ***tsrm_ls
#define TSRMLS_DC     , void ***tsrm_ls
#define TSRMLS_C     tsrm_ls
#define TSRMLS_CC     , tsrm_ls

非ZTS構建對下面的代碼看到的是兩個參數:int, char *。在ZTS構建下,原型則包含三個參數:int, char *, void ***。當你的程序調用這個函數時,只有在ZTS啓用時才需要傳遞第三個參數。下面代碼的第二行展示了宏的展開:

int php_myext_action(int action_id, char *message TSRMLS_DC);
php_myext_action(42, "The meaning of life" TSRMLS_CC);

通過在函數調用中包含這個特殊的變量,php_myext_action就可以使用tsrm_ls的值和MYEXT_G()宏函數一起訪問它的線程特有全局數據。在非ZTS構建上,tsrm_ls將不可用,但是這是ok的,因爲此時MYEXT_G()宏函數以及其他類似的宏都不會使用它。

現在考慮,你在一個新的擴展上工作,並且有下面的函數,它可以在你本地使用CLI SAPI的構建上正常工作,並且即便使用apache 1的apxs SAPI編譯也可以正常工作:

static int php_myext_isset(char *varname, int varname_len)
{
    zval **dummy;


    if (zend_hash_find(EG(active_symbol_table),
        varname, varname_len + 1,
        (void**)&dummy) == SUCCESS) {
        /* Variable exists */
        return 1;
    } else {
        /* Undefined variable */
        return 0;
    }
}

所有的一切看起來都工作正常,你打包這個擴展發送給他人構建並運行在生產服務器上。讓你氣餒的是,對方報告擴展編譯失敗。

事實上它們使用了Apache 2.0的線程模式,因此它們的php構建啓用了ZTS。當編譯期碰到你使用的EG()宏函數時,它嘗試在本地空間查找tsrm_ls沒有找到,因爲你並沒有定義它並且沒有在你的函數中傳遞。

修復這個問題非常簡單;只需要在php_myext_isset()的定義上增加TSRMLS_DC,並在每行調用它的地方增加TSRMLS_CC。不幸的是,現在對方已經有點不信任你的擴展質量了,這樣就會推遲你的演示週期。這種問題越早解決越好。

現在有了enable-maintainer-zts指令。通過在./configure時增加該指令來構建php,你的構建將自動的包含ZTS,哪怕你當前的SAPI(比如CLI)不需要它。打開這個開關,你可以避免這些常見的不應該出現的錯誤。

注意:在PHP4中,enable-maintainer-zts標記等價的名字是enable-experimental-zts;請確認使用你的php版本對應的正確標記。

尋回丟失的tsrm_ls

有時,我們需要在一個函數中使用tsrm_ls指針,但卻不能傳遞它。通常這是因爲你的擴展作爲某個使用回調的庫的接口,它並沒有提供返回抽象指針的地方。考慮下面的代碼片段:

void php_myext_event_callback(int eventtype, char *message)
{
    zval *event;


    /* $event = array('event'=>$eventtype,
                    'message'=>$message) */
    MAKE_STD_ZVAL(event);
    array_init(event);
    add_assoc_long(event, "type", eventtype);
    add_assoc_string(event, "message", message, 1);


    /* $eventlog[] = $event; */
    add_next_index_zval(EXT_G(eventlog), event);
}
PHP_FUNCTION(myext_startloop)
{
    /* The eventlib_loopme() function,
     * exported by an external library,
     * waits for an event to happen,
     * then dispatches it to the
     * callback handler specified.
     */
    eventlib_loopme(php_myext_event_callback);
}

雖然你可能不完全理解這段代碼,但你應該注意到了回調函數中使用了EXT_G()宏函數,我們知道在線程安全構建下它需要tsrm_ls指針。修改函數原型並不好也不應該這樣做,因爲外部的庫並不知道php的線程安全模型。那這種情況下怎樣讓tsrm_ls可用呢?

解決方案是前面提到的名爲TSRMLS_FETCH()的Zend宏函數。將它放到代碼片段的頂部,這個宏將執行給予當前線程上下文的查找,並定義本地的tsrm_ls指針拷貝。

這個宏可以在任何地方使用並且不用通過函數調用傳遞tsrm_ls,儘管這看起來很誘人,但是,要注意到這一點:TSRMLS_FETCH調用需要一定的處理時間。這在單次迭代中並不明顯,但是隨着你的線程數增多,隨着你調用TSRMLS_FETCH()的點的增多,你的擴展就會顯現出這個瓶頸。因此,請謹慎的使用它。

注意:爲了和c++編譯器兼容,請確保將TSRMLS_FETCH()和所有變量定義放在給定塊作用域的頂部(任何其他語句之前)。因爲TSRMLS_FETCH()宏自身有多種不同的解析方式,因此最好將它作爲變量定義的最後一行。

小結

本章中主要是對後續章節將要解釋的各種概念的一個概覽. 你還應該對整件事建立了基礎的認識, 它不只是要構建擴展, 還有幕後的Zend引擎和TSRM層, 它們將使你在將php嵌入到你的應用時獲利.


目錄

下一章: 變量的裏裏外外

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