首頁  >  文章  >  後端開發  >  探索PHP 生命週期

探索PHP 生命週期

coldplay.xixi
coldplay.xixi轉載
2020-07-28 16:38:082044瀏覽

探索PHP 生命週期

##學習 PHP 生命週期

PHP的生命週期是一個非常複雜的過程,其生命週期應該被熱衷於使用它的人所熱衷於使用它的人所熱衷於使用它的人。主要內容如下:

PHP 啟動。如果執行的是 CLI 或 FPM,它將執行 C 

main()。如果作為模組運行到網路伺服器,像使用 apxs2 SAPI (Apache 2),則 PHP 在 Apache 啟動後不久啟動,並開始運行其模組的啟動序列,PHP 就是其中之一。在內部稱啟動為模組啟動步驟。我們也將其縮寫為MINIT步驟。

一旦啟動,PHP 將等待處理一個/幾個請求。當我們談論 PHP CLI時,將只有一個請求:當前腳本要運行。但是,當我們談論 Web 環境時——應該是 PHP-FPM 或 Web 伺服器模組——PHP 可以一個接一個地處理多個請求。這完全依賴你如何配置你的 Web 伺服器:你可以告訴它處理無限數量的請求,或在關閉並回收該過程之前處理特定數量的請求。每次一個新的請求在執行緒中要處理時,PHP 就會執行

請求啟動步驟。我們稱之為 RINIT

相關學習推薦:

PHP程式設計從入門到精通

請求得到處理,(可能)產生了一些內容,OK。是時候關閉請求,並準備好處理另一個請求。關閉請求呼叫

請求關閉步驟。我們稱之為RSHUTDOWN。 ·

當處理完X個請求(一個,幾十個,數千個等),PHP 最後會自行關閉,然後結束。關閉 PHP 程序稱為

模組關閉步驟。縮寫為 MSHUTDOWN

如果我們可以畫出這些步驟,則可能會得到以下資訊:

探索PHP 生命週期

#並行模型

在CLI 環境,任何事都很容易:一個進程處理一個請求:它會啟動一個單獨的PHP 腳本,然後結束。 CLI 環境是 Web 環境的一種特殊化,它更為複雜。

為了同時處理多個請求,你必須執行並行模型。在PHP 中存在兩種:

    The process-based model 基於流程的模型
  • The thread-based model 基於執行緒的模型
使用基於進程的模型,作業系統將每個PHP 解釋器隔離到自己的進程中。這種模型在 Unix 非常普遍。每個請求都到它自己的進程。 PHP-CLI、PHP-FPM 和 PHP-CGI 使用此模型。

在基於執行緒的模型中,每個 PHP 解釋器都使用執行緒庫隔離到執行緒中。這個模型主要用在 Windows 作業系統,但也可以用在大多數的 Unix。要求 PHP 和其擴充在 ZTS 模式下被建構。

這是基於行程的模型:

探索PHP 生命週期

這是一個基於執行緒的模型:

探索PHP 生命週期

注意

作為擴充開發者,PHP 的多進程模組不是你的選擇。你將需要支持它。你必須讓你的擴充功能支援在執行緒環境中運行,特別是在 Windows平台下,並且必須針對它進行程式設計。

PHP 擴充鉤子

你可能猜到了,PHP 引擎將在多個生命週期點觸發你的擴充。我們稱它們為

鉤子函數。你的擴充功能可以在向引擎註冊時,透過聲明函數鉤子來聲明對特定生命週期點的興趣。 當你分析PHP 擴展結構時(
zend_module_entry 結構),這些鉤子可以清楚地看到:

struct _zend_module_entry {
        unsigned short size;
        unsigned int zend_api;
        unsigned char zend_debug;
        unsigned char zts;
        const struct _zend_ini_entry *ini_entry;
        const struct _zend_module_dep *deps;
        const char *name;
        const struct _zend_function_entry *functions;
        int (*module_startup_func)(INIT_FUNC_ARGS);        /* MINIT() */
        int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);   /* MSHUTDOWN() */
        int (*request_startup_func)(INIT_FUNC_ARGS);       /* RINIT() */
        int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);  /* RSHUTDOWN() */
        void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);     /* PHPINFO() */
        const char *version;
        size_t globals_size;
#ifdef ZTS
        ts_rsrc_id* globals_id_ptr;
#else
        void* globals_ptr;
#endif
        void (*globals_ctor)(void *global);                /* GINIT() */
        void (*globals_dtor)(void *global);                /* GSHUTDOWN */
        int (*post_deactivate_func)(void);                 /* PRSHUTDOWN() */
        int module_started;
        unsigned char type;
        void *handle;
        int module_number;
        const char *build_id;
};

現在讓我們看看你應該在這些鉤子中寫哪一個代碼。

模組初始化:MINIT()

這是 PHP 程式啟動步驟。在擴充的

MINIT()中,你將載入並指派以後每次請求需要的任何持久性物件或資訊。它們的大部分將分配為唯讀物件。

MINIT()中,尚未有線程或進程彈出,所以你完全可以存取全域變量,而沒有任何保護。另外,由於請求尚未啟動,因此你不能分配請求綁定的記憶體。你永遠不會在MINIT()步驟中使用Zend 記憶體管理 分配,但會使用永久分配。不是 emalloc(),而是pemalloc()。否則會導致崩潰。

在 MINIT()中,執行引擎仍未啟動,所以不要在沒有特別注意的情況下,嘗試存取其任何結構。

如果你需要為你的擴充註冊 INI 入口,則MINIT() 是正確的做法。

如果你要為以後使用而註冊只讀zend_strings,請使用持久分配了。

如果你需要分配的物件在處理請求時會寫入,那麼你必須複製它們的記憶體分配到該請求的執行緒專用池。記住,你只可以在MINIT()中安全地寫入全域空間。

注意

記憶體管理、分配和偵錯是記憶體管理的部分章節。

在 php_module_startup()函數中,透過zend_startup_modules()觸發 MINIT()

模組終止:MSHUTDOWN()

這是 PHP 進程終止步驟。很容易, 基本上,你在這裡運行了與MINIT()中使用的相反的操作。你釋放了資源,取消 INI 設定的註冊等等。

再次注意:執行引擎是關閉的,所以你不應該在此處存取其任何變數。

由於你在此處不需要請求,所以不應使用Zend 記憶體管理 的efree() 或類似函數去釋放資源,但對於釋放持久分配,使用pefree ()

 在php_module_shutdown()函數中,由zend_shutdown()zend_destroy_modules()中觸發MSHUTDOWN()

請求初始化: RINIT()

剛剛看過的請求,PHP 將在這裡處理它。在RINIT()中,你引導了處理該精確請求所需的資源。 PHP 是一種無共享架構,它提供了記憶體管理功能。

在 RINIT()中,如果需要分配動態內存,你將使用Zend 記憶體管理器。你將會呼叫 emalloc()。 Zend 記憶體管理器 追蹤你透過它分配的內存,當請求關閉時,如果你忘記這麼做,它將嘗試釋放請求綁定的記憶體(你不應該這麼做)。

在這裡,你不應要求持久的動態內存,即 libc 的malloc() 或Zend 的pemalloc()。如果你在這裡請求持久內存,並且忘記釋放它,則將造成洩露,並且隨著 PHP 處理越來越多的請求而堆積,最終導致進程崩潰(Kernel OOM) ,並且導致機器內存不足。

另外,務必注意不要在這裡寫入全域空間。如果 PHP 作為選定的並行模型運行到線程中,那麼你將修改每個線程池中的上下文(所有與你的請求並行處理的請求),並且如果你沒有鎖定內存,也可能觸發競爭條件。如果你需要全局,你必須保護它們。

注意

全域範圍管理解釋在專用章節。

在php_request_startup()函數中,透過zend_activate_module()觸發RINIT()

請求終止: RSHUTDOWN()

這是 PHP 請求終止步驟。 PHP 剛結束處理其請求,現在來清理其部分作為無共享架構的記憶體。接下來的請求不應記住當前請求的任何內容。很容易,基本上,你在此處執行了與RINIT()使用的相反的操作。你釋放了請求綁定的資源。

由於你在此處使用了請求,你應該使用 Zend 記憶體管理器的efree()或類似方式釋放資源。如果你忘記釋放並且造成洩露,在偵錯版本下,記憶體管理器將在進程stderr上記錄關於洩露的指標的日記,並且將為你釋放它們。

給你個主意,RSHUTDOWN()將被呼叫:

  • 執行用戶區關閉功能後(register_shutdown_function())
  • 在呼叫每個物件析構函數之後
  • PHP 輸出緩衝區刷新之後
  • 停用max_execution_time 之後

在php_request_shutdown()函數中,透過zend_deactivate_modules()觸發RSHUTDOWN()

Post 請求終止: PRSHUTDOWN()

這個鉤子很少使用。它在 RSHUTDOWN()之後調用,但中間還會運行一些額外的引擎程式碼。
尤其是在Post-RSHUTDOWN 中:

  • PHP 輸出緩衝區已關閉,並且它的處理程序已刷新
  • PHP 超全域已經銷毀
  • 執行引擎已經關閉

這個鉤子很少使用。在php_request_shutdown()函數中,透過zend_post_deactivate_modules(),在RSHUTDOWN()之後被觸發。

全域初始化: GINIT()

執行緒庫每次彈出執行緒時都會呼叫該鉤子。如果你使用多進程,當 PHP 啟動,僅在觸發 MINIT() 之前呼叫此函數。

這裡不講太多細節,只需在這裡簡單地初始化全域變量,通常初始化為0。全局管理將在專用章節中詳細說明。

記住,全域變數不會在每次請求後清理。如果你需要為每次新的請求重置它們(可能),那麼你必須將這樣地進程放到RINIT()

注意

全域範圍管理在專用章節詳細介紹。

全域終止: GSHUTDOWN()

在執行緒庫中,每當執行緒終止時都會呼叫該鉤子。如果你使用多線程,函數將在 PHP 終止期間(在MSHUTDOWN())被呼叫一次。

在這裡不提供太多細節,你只需簡單地在這裡取消初始化你的全域變量,通常你不必做什麼,但如果在建立全域(GINIT())時分配了資源,在這裡的步驟你應該釋放它們。

全域管理將在專用章節詳細介紹。

記住,全域變數在每次請求後不會清除。即GSHUTDOWN()不會被呼叫為RSHUTDOWN()的一部分。

注意

全域範圍管理在專用章節有詳細介紹。

資訊收集: MINFO()

該鉤子很特殊,它永遠不會被引擎自動觸發,只有你詢問它有關擴展的資訊時才會觸發。典型的例子是呼叫phpinfo()。然後運行此函數,並將有關當前擴展的特殊資訊列印到流中。

簡而言之,phpinfo() 展示資訊。

該函數也可以透過 CLI 使用反射開關之一調用,例如php --ri pib 或透過使用者區調用ini_get_all()

你可以將其留空,在這種情況下,只有擴展的名字顯示,沒有其他(可能不會顯示 INI 設置,因為這是 MINFO() 的一部分)。

關於PHP 生命週期的思考

探索PHP 生命週期

你可能已經發現了,RINIT() 和 RSHUTDOWN() 尤其重要,因為它們在擴展中被觸發成千上萬次。如果 PHP 步驟是關於 Web (不是 CLI),並且已經配置為可以處理無數次請求,那麼你的 RINIT()/RSHUTDOWN() 組將會被無數次呼叫。

我們想要再次引起你對記憶體管理的關注。在處理請求時(在RINIT() 和 RSHUTDOWN()之間),你最終洩漏的小字節,將對滿載伺服器產生嚴重影響。這就是為什麼建議你使用 Zend 記憶體管理器 進行此類分配,並且準備好調試記憶體佈局。作為無共享架構的一部分,PHP 在每次請求最後都會忘記並釋放請求內存,這是 PHP 的內部設計。

另外,如果你的崩潰訊號是 SIGSEGV (壞記憶體存取),則整個進程會崩潰。如果 PHP 是使用執行緒作為多進程引擎,那麼你所有其他執行緒也會崩潰,甚至可能造成伺服器崩潰。

注意

C 語言不是 PHP 語言。使用 C,在程式的錯誤很可能導致程式的崩潰與終止。

透過重寫函數指標進行掛鉤

現在你知道引擎何時會觸發程式碼,也存在值得注意的函數指針,你可以取代它們來掛載到引擎。因為那些指標是全域變量,因此你可以將它們替換為 MINIT() 步驟,並將它們放回MSHUTDOWN()

有興趣的有:

  • AST, Zend/zend_ast.h:

    • void (zend_ast_process_t) (zend_ast ast)
  • Compiler, Zend/zend_compile.h:

    • zend_op_array ( zend_compile_file)(zend_file_handle file_handle, int type)*
    • zend_op_array (zend_compile_string)(zval source_string, char filename)#char##filename)
  • ##Executor, Zend/zend_execute.h:

    • void (zend_execute_ex)(zend_execute_data execute_data)
    • void (zend_execute_internal)(zend_execute_data execute_data, zval return_value)*
  • GC, Zend/zend_gc.h:

    • int (gc_collect_cycles)(void)*
  • TSRM, TSRM/TSRM. h:

    • void (tsrm_thread_begin_func_t)(THREAD_T thread_id)*
    • void (tsrm_thread_end_func_t)(THREAD_T thread_id)*
  • ##Error, Zend/zend.h:
    • void (
    • zend_error_cb)(int type, const char error_filename, const uint error_lineno, const char format, va_​​list args)*
  • Exceptions, Zend/zend_exceptions.h:
    • void (zend_throw_exception_hook)(zval ex)
  • Lifetime, Zend/zend.h:

    • void (zend_on_timeout)(int seconds)*
    • void (zend_interrupt_function)(zend_execute_data execute_data)
    • void (zend_ticks_function)(int ticks)*

還有其他存在,但上面的是最重要的,當你設計PHP 擴充時,你可能需要。因為它們的名字很容易看,所以不再詳細解釋它們。

如果你需要更多信息,你可以在 PHP 原始碼查看,並發現何時和如何觸發它們。

以上是探索PHP 生命週期的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:learnku.com。如有侵權,請聯絡admin@php.cn刪除