SAPI指的是PHP具體應用的編程接口,就像PC同樣,不管安裝哪些操做系統,只要知足了PC的接口規範均可以在PC上正常運行。PHP腳本要執行有不少種方式,經過Web服務器,或者直接在命令行下,也能夠嵌入在其餘程序中。php
一般,咱們使用Apache或者Nginx這類Web服務器來測試PHP腳本,或者在命令行下經過PHP解釋器程序來執行。 腳本執行完後,Web服務器應答,瀏覽器顯示應答信息,或者在命令行標準輸出上顯示內容。mysql
咱們不多關心PHP解釋器在哪裏。雖然經過Web服務器和命令行程序執行腳本看起來很不同, 實際上它們的工做流程是同樣的。命令行參數傳遞給PHP解釋器要執行的腳本, 至關於經過url請求一個PHP頁面。腳本執行完成後返回響應結果,只不過命令行的響應結果是顯示在終端上。 web
腳本執行的開始都是以SAPI接口實現開始的。只是不一樣的SAPI接口實現會完成他們特定的工做, 例如Apache的mod_php SAPI實現須要初始化從Apache獲取的一些信息,在輸出內容是將內容返回給Apache, 其餘的SAPI實現也相似。sql
PHP開始執行之後會通過兩個主要的階段:處理請求以前的開始階段和請求以後的結束階段。 開始階段有兩個過程:第一個過程是模塊初始化階段(MINIT), 在整個SAPI生命週期內(例如Apache啓動之後的整個生命週期內或者命令行程序整個執行過程當中), 該過程只進行一次。第二個過程是模塊激活階段(RINIT),該過程發生在請求階段, 例如經過url請求某個頁面,則在每次請求以前都會進行模塊激活(RINIT請求開始)。 例如PHP註冊了一些擴展模塊,則在MINIT階段會回調全部模塊的MINIT函數。 模塊在這個階段能夠進行一些初始化工做,例如註冊常量,定義模塊使用的類等等。 模塊在實現時能夠經過以下宏來實現這些回調函數:apache
PHP_MINIT_FUNCTION(myphpextension) { // 註冊常量或者類等初始化操做 return SUCCESS; }
請求到達以後PHP初始化執行腳本的基本環境,例如建立一個執行環境,包括保存PHP運行過程當中變量名稱和值內容的符號表, 以及當前全部的函數以及類等信息的符號表。而後PHP會調用全部模塊的RINIT函數, 在這個階段各個模塊也能夠執行一些相關的操做,模塊的RINIT函數和MINIT回調函數相似:編程
PHP_RINIT_FUNCTION(myphpextension) { // 例如記錄請求開始時間 // 隨後在請求結束的時候記錄結束時間。這樣咱們就可以記錄下處理請求所花費的時間了 return SUCCESS; }
請求處理完後就進入告終束階段,通常腳本執行到末尾或者經過調用exit()或die()函數, PHP都將進入結束階段。和開始階段對應,結束階段也分爲兩個環節,一個在請求結束後停用模塊(RSHUTDOWN,對應RINIT), 一個在SAPI生命週期結束(Web服務器退出或者命令行腳本執行完畢退出)時關閉模塊(MSHUTDOWN,對應MINIT)。windows
PHP_RSHUTDOWN_FUNCTION(myphpextension) { // 例如記錄請求結束時間,並把相應的信息寫入到日至文件中。 return SUCCESS; }
想要了解擴展開發的相關內容,請參考第十三章 擴展開發api
CLI/CGI模式的PHP屬於單進程的SAPI模式。這類的請求在處理一次請求後就關閉。也就是隻會通過以下幾個環節: 開始 - 請求開始 - 請求關閉 - 結束 SAPI接口實現就完成了其生命週期。如圖2.1所示:數組
如上的圖是很是簡單,也很好理解。只是在各個階段之間PHP還作了許許多多的工做。這裏作一些補充:瀏覽器
啓動
在調用每一個模塊的模塊初始化前,會有一個初始化的過程,它包括:
這裏的初始化全局變量大多數狀況下是將其設置爲NULL,有一些除外,好比設置zuf(zend_utility_functions), 以zuf.printf_function = php_printf爲例,這裏的php_printf在zend_startup函數中會被賦值給zend_printf做爲全局函數指針使用, 而zend_printf函數一般會做爲常規字符串輸出使用,好比顯示程序調用棧的debug_print_backtrace就是使用它打印相關信息。
這裏的常量是PHP本身的一些常量,這些常量要麼是硬編碼在程序中,好比PHP_VERSION,要麼是寫在配置頭文件中, 好比PEAR_EXTENSION_DIR,這些是寫在config.w32.h文件中。
前面提到的zend_startup函數的做用就是初始化Zend引擎,這裏的初始化操做包括內存管理初始化、 全局使用的函數指針初始化(如前面所說的zend_printf等),對PHP源文件進行詞法分析、語法分析、 中間代碼執行的函數指針的賦值,初始化若干HashTable(好比函數表,常量表等等),爲ini文件解析作準備, 爲PHP源文件解析作準備,註冊內置函數(如strlen、define等),註冊標準常量(如E_ALL、TRUE、NULL等)、註冊GLOBALS全局變量等。
php_init_config函數的做用是讀取php.ini文件,設置配置參數,加載zend擴展並註冊PHP擴展函數。此函數分爲以下幾步: 初始化參數配置表,調用當前模式下的ini初始化配置,好比CLI模式下,會作以下初始化:
INI_DEFAULT("report_zend_debug", "0");
INI_DEFAULT("display_errors", "1");
不過在其它模式下卻沒有這樣的初始化操做。接下來會的各類操做都是查找ini文件:
php.ini 的搜索路徑以下(按順序):
php_startup_auto_globals函數會初始化在用戶空間所使用頻率很高的一些全局變量,如:$_GET、$_POST、$_FILES等。 這裏只是初始化,所調用的zend_register_auto_global函數也只是將這些變量名添加到CG(auto_globals)這個變量表。
php_startup_sapi_content_types函數用來初始化SAPI對於不一樣類型內容的處理函數, 這裏的處理函數包括POST數據默認處理函數、默認數據處理函數等。
php_register_internal_extensions_func函數用來註冊靜態構建的模塊,也就是默認加載的模塊, 咱們能夠將其認爲內置模塊。在PHP5.3.0版本中內置的模塊包括PHP標準擴展模塊(/ext/standard/目錄, 這裏是咱們用的最頻繁的函數,好比字符串函數,數學函數,數組操做函數等等),日曆擴展模塊、FTP擴展模塊、 session擴展模塊等。這些內置模塊並非一成不變的,在不一樣的PHP模板中,因爲不一樣時間的需求或其它影響因素會致使這些默認加載的模塊會變化, 好比從代碼中咱們就能夠看到mysql、xml等擴展模塊曾經或未來會做爲內置模塊出現。
模塊初始化會執行兩個操做: 1. 將這些模塊註冊到已註冊模塊列表(module_registry),若是註冊的模塊已經註冊過了,PHP會報Module XXX already loaded的錯誤。 2. 將每一個模塊中包含的函數註冊到函數表( 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作請求初始化操做。 請求初始化操做,除了圖中顯示的調用每一個模塊的請求初始化函數外,還作了較多的其它工做,其主要內容以下:
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_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函數中。 這個集合包括以下內容:
結束
最終到了要收尾的地方了。
sapi_flush將最後的內容刷新出去。其調用的是sapi_module.flush,在CLI模式下等價於fflush函數。
zend_shutdown將關閉Zend引擎。
此時對應圖中的流程,咱們應該是執行每一個模塊的關閉模塊操做。 在這裏只有一個zend_hash_graceful_reverse_destroy函數將module_registry銷燬了。 固然,它最終也是調用了關閉模塊的方法的,其根源在於在初始化module_registry時就設置了這個hash表析構時調用ZEND_MODULE_DTOR宏。 而ZEND_MODULE_DTOR宏對應的是module_destructor函數。 在此函數中會調用模塊的module_shutdown_func方法,即PHP_RSHUTDOWN_FUNCTION宏產生的那個函數。
在關閉全部的模塊後,PHP繼續銷燬全局函數表,銷燬全局類表、銷售全局變量表等。 經過zend_shutdown_extensions遍歷zend_extensions全部元素,調用每一個擴展的shutdown函數。
一般PHP是編譯爲apache的一個模塊來處理PHP請求。Apache通常會採用多進程模式, Apache啓動後會fork出多個子進程,每一個進程的內存空間獨立,每一個子進程都會通過開始和結束環節, 不過每一個進程的開始階段只在進程fork出來以來後進行,在整個進程的生命週期內可能會處理多個請求。 只有在Apache關閉或者進程被結束以後纔會進行關閉階段,在這兩個階段之間會隨着每一個請求重複請求開始-請求關閉的環節。 如圖2.2所示:
多線程模式和多進程中的某個進程相似,不一樣的是在整個進程的生命週期內會並行的重複着 請求開始-請求關閉的環節
Zend引擎是PHP實現的核心,提供了語言實現上的基礎設施。例如:PHP的語法實現,腳本的編譯運行環境, 擴展機制以及內存管理等,固然這裏的PHP指的是官方的PHP實現(除了官方的實現, 目前比較知名的有facebook的hiphop實現,不過到目前爲止,PHP尚未一個標準的語言規範), 而PHP則提供了請求處理和其餘Web服務器的接口(SAPI)。
目前PHP的實現和Zend引擎之間的關係很是緊密,甚至有些過於緊密了,例如不少PHP擴展都是使用的Zend API, 而Zend正是PHP語言自己的實現,PHP只是使用Zend這個內核來構建PHP語言的,而PHP擴展大都使用Zend API, 這就致使PHP的不少擴展和Zend引擎耦合在一塊兒了,在筆者編寫這本書的時候PHP核心開發者就提出將這種耦合解開,
目前PHP的受歡迎程度是毋庸置疑的,但凡流行的語言一般都會出現這個語言的其餘實現版本, 這在Java社區裏就很是明顯,目前已經有很是多基於JVM的語言了,例如IBM的Project Zero就實現了一個基於JVM的PHP實現, .NET也有相似的實現,一般他們這樣作的緣由無非是由於:他們喜歡這個語言,但又不想放棄原有的平臺, 或者對現有的語言實現不滿意,處於性能或者語言特性等(HipHop就是這樣誕生的)。
不少腳本語言中都會有語言擴展機制,PHP中的擴展一般是經過Pear庫或者原生擴展,在Ruby中則這二者的界限不是很明顯, 他們甚至會提供兩套實現,一個主要用於在沒法編譯的環境下使用,而在合適的環境則使用C實現的原生擴展, 這樣在效率和可移植性上均可以保證。目前這些爲PHP編寫的擴展一般都沒法在其餘的PHP實現中實現重用, HipHop的作法是對最爲流行的擴展進行重寫。若是PHP擴展能和ZendAPI解耦,則在其餘語言中重用這些擴展也將更加容易了。