PHP核心的學習--PHP生命週期

WBOY
發布: 2016-08-08 09:23:50
原創
1026 人瀏覽過

一切的開始: SAPI介面

SAPI(Server Application Programming Interface)指的是PHP具體應用的編程接口, 就像PC一樣,無論安裝哪些作業系統,只要滿足了PC的接口規範都可以在PC上正常運行, PHP腳本要執行有很多種方式,透過Web伺服器,或直接在命令列下,也可以嵌入在其他程式中。

通常,我們使用Apache或Nginx這類Web伺服器來測試PHP腳本,或是在命令列下透過PHP解釋器程式來執行。 腳本執行完後,Web伺服器應答,瀏覽器顯示應答訊息,或在命令列標準輸出上顯示內容。

我們很少關心PHP解釋器在哪裡。雖然透過Web伺服器和命令列程式執行腳本看起來很不一樣, 實際上它們的工作流程是一樣的。命令列參數傳遞給PHP解釋器要執行的腳本, 相當於透過url請求一個PHP頁面。腳本執行完成後回傳回應結果,只不過命令列的回應結果是顯示在終端機上。

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

下面幾個小節將對一些常見的SAPI實作進行更深入的介紹。

開始和結束

PHP開始執行以後會經過兩個主要的階段:處理請求之前的開始階段和請求之後的結束階段。 開始階段有兩個過程:第一個過程是模組初始化階段(MINIT), 在整個SAPI生命週期內(例如Apache啟動以後的整個生命週期內或命令列程式整個執行過程中), 過程只進行一次。第二個過程是模組啟動階段(RINIT),該過程發生在請求階段, 例如透過url請求某個頁面,則在每次請求之前都會進行模組啟動(RINIT請求開始)。 例如PHP註冊了一些擴充模組,則在MINIT階段會回呼所有模組的MINIT函數。 模組在這個階段可以進行一些初始化工作,例如註冊常數,定義模組使用的類別等等。 模組在實作時可以透過以下巨集來實現這些回呼函數:

<span>PHP_MINIT_FUNCTION(myphpextension)
{
    </span><span>//</span><span> 注册常量或者类等初始化操作</span><span>return</span><span> SUCCESS; 
}</span>
登入後複製

請求到達之後PHP初始化執行腳本的基本環境,例如建立執行環境,包括保存PHP運行過程中變數名稱和值內容的符號表, 以及目前所有的函數以及類別等資訊的符號表。然後PHP會呼叫​​所有模組的RINIT函數, 在這個階段各個模組也可以執行一些相關的操作,模組的RINIT函數和MINIT回呼函數類似:

<span>PHP_RINIT_FUNCTION(myphpextension)
{
    </span><span>//</span><span> 例如记录请求开始时间
    </span><span>//</span><span> 随后在请求结束的时候记录结束时间。这样我们就能够记录下处理请求所花费的时间了</span><span>return</span><span> SUCCESS; 
}</span>
登入後複製

請求處理完後就進入了結束階段,一般腳本執行到末尾或透過呼叫exit()或die()函數, PHP都將進入結束階段。和開始階段對應,結束階段也分為兩個環節,一個在請求結束後停用模組(RSHUTDOWN,對應RINIT),一個在SAPI生命週期結束(Web伺服器退出或命令列腳本執行完畢退出)時關閉模組(MSHUTDOWN,對應MINIT)。

<span>PHP_RSHUTDOWN_FUNCTION(myphpextension)
{
    </span><span>//</span><span> 例如记录请求结束时间,并把相应的信息写入到日至文件中。</span><span>return</span><span> SUCCESS; 
}</span>
登入後複製

單一進程SAPI生命週期

CLI/CGI模式的PHP屬於單一進程的SAPI模式。這類的請求在處理一次請求後就關閉。也就是只會經過以下幾個環節: 開始 - 請求開始 - 請求關閉 - 結束 SAPI介面實作就完成了其生命週期。如下圖:

 

單一進程SAPI生命週期

如上的圖非常簡單,也很容易理解。只是在各階段之間PHP還做了許許多多的工作。這裡做一些補充:

啟動

在呼叫每個模組的模組初始化前,會有一個初始化的過程,它包括:

  • 初始化若干全域變數

這裡的初始化全域變數大多數情況下是將其設為NULL,有一些除外,例如設定zuf(zend_utility_functions), 以zuf.printf_function = php_printf為例,這裡的php_printf在zend_startup函數中會被賦值給zend_printf作為全域函數指標使用,而作為全域函數指標常規字串輸出使用,例如顯示程式呼叫堆疊的debug_print_backtrace就是使用它來列印相關資訊。

  • 初始化若干常數

這裡的常數是PHP自己的一些常數,這些常數要么是硬編碼在程序中,比如PHP_VERSION,要么是寫在配置頭文件中, 比如PEAR_EXTENSION_DIR,這些是寫在config. w32.h檔案中。

  • 初始化Zend引擎和核心组件

前面提到的zend_startup函数的作用就是初始化Zend引擎,这里的初始化操作包括内存管理初始化、 全局使用的函数指针初始化(如前面所说的zend_printf等),对PHP源文件进行词法分析、语法分析、 中间代码执行的函数指针的赋值,初始化若干HashTable(比如函数表,常量表等等),为ini文件解析做准备, 为PHP源文件解析做准备,注册内置函数(如strlen、define等),注册标准常量(如E_ALL、TRUE、NULL等)、注册GLOBALS全局变量等。

  • 解析php.ini

php_init_config函数的作用是读取php.ini文件,设置配置参数,加载zend扩展并注册PHP扩展函数。此函数分为如下几步: 初始化参数配置表,调用当前模式下的ini初始化配置,比如CLI模式下,会做如下初始化:

INI_DEFAULT(<span>"</span><span>report_zend_debug</span><span>"</span>, <span>"</span><span>0</span><span>"</span><span>);
INI_DEFAULT(</span><span>"</span><span>display_errors</span><span>"</span>, <span>"</span><span>1</span><span>"</span>);
登入後複製

不过在其它模式下却没有这样的初始化操作。接下来会的各种操作都是查找ini文件:

  1. 判断是否有php_ini_path_override,在CLI模式下可以通过-c参数指定此路径(在php的命令参数中-c表示在指定的路径中查找ini文件)。
  2. 如果没有php_ini_path_override,判断php_ini_ignore是否为非空(忽略php.ini配置,这里也就CLI模式下有用,使用-n参数)。
  3. 如果不忽略ini配置,则开始处理php_ini_search_path(查找ini文件的路径),这些路径包括CWD(当前路径,不过这种不适用CLI模式)、 执行脚本所在目录、环境变量PATH和PHPRC和配置文件中的PHP_CONFIG_FILE_PATH的值。
  4. 在准备完查找路径后,PHP会判断现在的ini路径(php_ini_file_name)是否为文件和是否可打开。 如果这里ini路径是文件并且可打开,则会使用此文件, 也就是CLI模式下通过-c参数指定的ini文件的优先级是最高的, 其次是PHPRC指定的文件,第三是在搜索路径中查找php-%sapi-module-name%.ini文件(如CLI模式下应该是查找php-cli.ini文件), 最后才是搜索路径中查找php.ini文件。
  • 全局操作函数的初始化

php_startup_auto_globals函数会初始化在用户空间所使用频率很高的一些全局变量,如:$_GET、$_POST、$_FILES等。 这里只是初始化,所调用的zend_register_auto_global函数也只是将这些变量名添加到CG(auto_globals)这个变量表。

php_startup_sapi_content_types函数用来初始化SAPI对于不同类型内容的处理函数, 这里的处理函数包括POST数据默认处理函数、默认数据处理函数等。

  • 初始化静态构建的模块和共享模块(MINIT)

php_register_internal_extensions_func函数用来注册静态构建的模块,也就是默认加载的模块, 我们可以将其认为内置模块。在PHP5.3.0版本中内置的模块包括PHP标准扩展模块(/ext/standard/目录, 这里是我们用的最频繁的函数,比如字符串函数,数学函数,数组操作函数等等),日历扩展模块、FTP扩展模块、 session扩展模块等。这些内置模块并不是一成不变的,在不同的PHP模板中,由于不同时间的需求或其它影响因素会导致这些默认加载的模块会变化, 比如从代码中我们就可以看到mysql、xml等扩展模块曾经或将来会作为内置模块出现。

模块初始化会执行两个操作: 1. 将这些模块注册到已注册模块列表(module_registry),如果注册的模块已经注册过了,PHP会报Module XXX already loaded的错误。 1. 将每个模块中包含的函数注册到函数表( CG(function_table) ),如果函数无法添加,则会报 Unable to register functions, unable to load。

在注册了静态构建的模块后,PHP会注册附加的模块,不同的模式下可以加载不同的模块集,比如在CLI模式下是没有这些附加的模块的。

在内置模块和附加模块后,接下来是注册通过共享对象(比如DLL)和php.ini文件灵活配置的扩展。

在所有的模块都注册后,PHP会马上执行模块初始化操作(zend_startup_modules)。 它的整个过程就是依次遍历每个模块,调用每个模块的模块初始化函数, 也就是在本小节前面所说的用宏PHP_MINIT_FUNCTION包含的内容。

  • 禁用函数和类

php_disable_functions函数用来禁用PHP的一些函数。这些被禁用的函数来自PHP的配置文件的disable_functions变量。 其禁用的过程是调用zend_disable_function函数将指定的函数名从CG(function_table)函数表中删除。

php_disable_classes函数用来禁用PHP的一些类。这些被禁用的类来自PHP的配置文件的disable_classes变量。 其禁用的过程是调用zend_disable_class函数将指定的类名从CG(class_table)类表中删除。

ACTIVATION

在處理了檔案相關的內容,PHP會呼叫​​php_request_startup做請求初始化操作。 請求初始化操作,除了圖中顯示的調用每個模組的請求初始化函數外,還做了較多的其它工作,其主要內容如下:

  • 激活Zend引擎

gc_reset函數用來重置垃圾收集機制,當然這是在PHP5.3之後才有的。

init_compiler函數用來初始化編譯器,例如將編譯過程中在放opcode的陣列清空,準備編譯時用來的資料結構等等。

init_executor函數用來初始化中間程式碼執行過程。 在編譯過程中,函數清單、類別清單等都存放在編譯時的全域變數中, 在準備執行過程時,會將這些清單賦值給執行的全域變數中,如:EG(function_table) = CG(function_table) ; 中間程式碼執行是在PHP的執行虛擬堆疊中,初始化時這些堆疊等都會一起被初始化。 除了棧,還有存放變數的符號表(EG(symbol_table))會被初始化為50個元素的hashtable,存放物件的EG(objects_store)被初始化了1024個元素。 PHP的執行環境除了上面的一些變數外,還有錯誤處理,異常處理等等,這些都是在這裡被初始化的。 透過php.ini配置的zend_extensions也是在這裡被遍歷呼叫activate函數。

  • 啟動SAPI

sapi_activate函數用來初始化SG(sapi_headers)和SG(request_info),並且針對HTTP請求的方法設定一些內容,當請求方法為HEAD時,設定SG(request_info).headers_only=1 ; 此函數最重要的一個操作是處理請求的數據,最終都會呼叫sapi_module.default_post_reader。 而sapi_module.default_post_reader在前面的模組初始化是透過php_startup_sapi_content_types函數註冊了 預設處理函數為main/php_content_types.c檔案中php_default_post_reader函數。 此函數會將POST的原始資料寫入$HTTP_RAW_POST_DATA變數。

在處理了post資料後,PHP會透過sapi_module.read_cookies讀取cookie的值, 在CLI模式下,此函數的實作為sapi_cli_read_cookies,而在函數體中卻只有一個return NULL;

如果當前模式下有設定activate函數,則執行此函數,啟動SAPI,在CLI模式下此函數指標設定為NULL。

  • 環境初始化

這裡的環境初始化是指在用戶空間中需要用到的一些環境變數初始化,這裡的環境包括伺服器環境、請求資料環境等。 實際到我們用到的變量,就是$_POST、$_GET、$_COOKIE、$_SERVER、$_ENV、$_FILES。 和sapi_module.default_post_reader一樣,sapi_module.treat_data的值也是在模組初始化時, 透過php_startup_sapi_content_types函數註冊了預設資料處理函數為main/php_variables.c檔案中php_default_treat_data函數。

以$_COOKIE為例,php_default_treat_data函數會對依據分隔符,將所有的cookie拆分並賦值給對應的變數。

  • 模組請求初始化

PHP透過zend_activate_modules函數實現模組的請求初始化,也就是我們在圖中看到Call each extension's RINIT。 此函數透過遍歷註冊在module_registry變數中的所有模組,呼叫其RINIT方法實作模組的請求初始化操作。

運行

php_execute_script函數包含了執行PHP腳本的全部過程。

當一個PHP文件需要解析執行時,它可能會需要執行三個文件,其中包括一個前置執行文件、當前需要執行的主文件和一個後置執行文件。 非目前的兩個檔案可以在php.ini檔案透過auto_prepend_file參數和auto_append_file參數設定。 如果將這兩個參數設為空,則會停用對應的執行檔。

對於需要解析執行的文件,透過zend_compile_file(compile_file函數)做詞法分析、語法分析和中間程式碼產生操作,傳回此文件的所有中間程式碼。 如果解析的檔案有產生有效的中間程式碼,則呼叫zend_execute(execute函數)執行中間程式碼。 如果在執行過程中出現異常且使用者有定義對這些異常的處理,則呼叫這些異常處理函數。 在所有的操作都處理完後,PHP透過EG(return_value_ptr_ptr)傳回結果。

DEACTIVATION

PHP關閉請求的過程是一個若干個關閉操作的集合,這個集合存在於php_request_shutdown函數中。 這個集合包括如下:

  1. 呼叫所有透過register_shutdown_function()註冊的函數。這些在關閉時呼叫的函數是在用戶空間添加進來的。 一個簡單的例子,我們可以在腳本出錯時呼叫一個統一的函數,給使用者一個友善一些的頁面,這個有點類似網頁中的404頁面。
  2. 執行所有可用的__destruct函數。 這裡的析構函數包括在物件池(EG(objects_store)中的所有物件的析構函數以及EG(symbol_table)中各個元素的析構方法。
  3. 將所有的輸出刷出去。
  4. 發送HTTP應答頭。 RSHUTDOWN。基本每個擴充的post_deactivate_func函數指標都是NULL。記憶體管理。下等價於fflush函數。將module_registry銷毀了。中會呼叫模組的module_shutdown_func方法,也就是PHP_RSHUTDOWN_FUNCTION巨集產生的那個函數。
  5. 在關閉所有的模組後,PHP繼續銷毀全域函數表,銷毀全域類別表、銷售全域變數表等。呼叫每個擴充的shutdown函數。 Apache一般會採用多進程模式, Apache啟動後會fork出多個子進程,每個進程的記憶體空間獨立,每個子進程都會經過開始和結束環節, 不過每個進程的開始階段只在進程fork出來以來進行,在整個進程的生命週期內可能會處理多個請求。 只有在Apache關閉或進程被結束之後才會進行關閉階段,在這兩個階段之間會隨著每個請求重複請求開始-請求關閉的環節。 如下圖所示:
  6.  
  7. 多進程SAPI生命週期
  8. 多執行緒的SAPI生命週期
  9. 多執行緒模式與多進程中的某個進程類似,不同的是在整個進程的生命週期中會並行的重複著請求開始-請求關閉的環節
  10.  

多執行緒SAPI生命週期

摘自:http://www.php-internals.com/book/?p=chapt02/02-01- php-life-cycle-and-zend-engine

  • 以上就介紹了PHP核心的學習--PHP生命週期,包含了面向的內容,希望對PHP教學有興趣的朋友有幫助。

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板