從SAPI接口開始 php
SAPI:Server Application Programming Interface 服務器端應用編程端口。研究過PHP架構的同窗應該知道這個東東的重要性,它提供了一個接口,使得PHP能夠和其餘應用進行交互數據。 本文不會詳細介紹每一個PHP的SAPI,只是針對最簡單的CGI SAPI,來講明SAPI的機制。 html
咱們先來看看PHP的架構圖: 前端
SAPI指的是PHP具體應用的編程接口, 就像PC同樣,不管安裝哪些操做系統,只要知足了PC的接口規範均可以在PC上正常運行, PHP腳本要執行有不少種方式,經過Web服務器,或者直接在命令行下,也能夠嵌入在其餘程序中。 mysql
一般,咱們使用Apache或者Nginx這類Web服務器來測試PHP腳本,或者在命令行下經過PHP解釋器程序來執行。 腳本執行完後,Web服務器應答,瀏覽器顯示應答信息,或者在命令行標準輸出上顯示內容。 nginx
咱們不多關心PHP解釋器在哪裏。雖然經過Web服務器和命令行程序執行腳本看起來很不同, 實際上它們的工做流程是同樣的。命令行參數傳遞給PHP解釋器要執行的腳本, 至關於經過url請求一個PHP頁面。腳本執行完成後返回響應結果,只不過命令行的響應結果是顯示在終端上。 web
腳本執行的開始都是以SAPI接口實現開始的。只是不一樣的SAPI接口實現會完成他們特定的工做, 例如Apache的mod_php SAPI實現須要初始化從Apache獲取的一些信息,在輸出內容是將內容返回給Apache, 其餘的SAPI實現也相似。 sql
SAPI提供了一個和外部通訊的接口, 對於PHP5.2,默認提供了不少種SAPI, 常見的給apache的mod_php5,CGI,給IIS的ISAPI,還有Shell的CLI,本文就從CGI SAPI入手 ,介紹SAPI的機制。 雖然CGI簡單,可是不用擔憂,它包含了絕大部份內容,足以讓你深入理解SAPI的工做原理。 數據庫
要定義個SAPI,首先要定義個sapi_module_struct, 查看 PHP-SRC/sapi/cgi/cgi_main.c: apache
02 |
staticsapi_module_struct cgi_sapi_module = { |
04 |
"cgi-fcgi", /* name */ |
05 |
"CGI/FastCGI", /* pretty name */ |
08 |
"CGI", /* pretty name */ |
11 |
php_cgi_startup, /* startup */ |
12 |
php_module_shutdown_wrapper, /* shutdown */ |
15 |
sapi_cgi_deactivate, /* deactivate */ |
17 |
sapi_cgibin_ub_write, /* unbuffered write */ |
18 |
sapi_cgibin_flush, /* flush */ |
20 |
sapi_cgibin_getenv, /* getenv */ |
22 |
php_error, /* error handler */ |
24 |
NULL, /* header handler */ |
25 |
sapi_cgi_send_headers, /* send headers handler */ |
26 |
NULL, /* send header handler */ |
28 |
sapi_cgi_read_post, /* read POST data */ |
29 |
sapi_cgi_read_cookies, /* read Cookies */ |
31 |
sapi_cgi_register_variables, /* register server variables */ |
32 |
sapi_cgi_log_message, /* Log message */ |
33 |
NULL, /* Get request time */ |
35 |
STANDARD_SAPI_MODULE_PROPERTIES |
這個結構,包含了一些常量,好比name, 這個會在咱們調用php_info()的時候被使用。一些初始化,收尾函數,以及一些函數指針,用來告訴Zend,如何獲取,和輸出數據。 編程
1. php_cgi_startup, 當一個應用要調用PHP的時候,這個函數會被調用,對於CGI來講,它只是簡單的調用了PHP的初始化函數:
1 |
staticintphp_cgi_startup(sapi_module_struct *sapi_module) |
3 |
if(php_module_startup(sapi_module, NULL, 0) == FAILURE) { |
2. php_module_shutdown_wrapper , 一個對PHP關閉函數的簡單包裝。只是簡單的調用php_module_shutdown;
3. PHP會在每一個request的時候,處理一些初始化,資源分配的事務。這部分就是activate字段要定義的,從上面的結構咱們能夠看出,對於CGI 來講,它並無提供初始化處理句柄。對於mod_php來講,那就不一樣了,他要在apache的pool中註冊資源析構函數, 申請空間, 初始化環境變量,等等。
4. sapi_cgi_deactivate, 這個是對應與activate的函數,顧名思義,它會提供一個handler, 用來處理收尾工做,對於CGI來講,他只是簡單的刷新緩衝區,用以保證用戶在Zend關閉前獲得全部的輸出數據:
01 |
staticintsapi_cgi_deactivate(TSRMLS_D) |
03 |
/* flush only when SAPI was started. The reasons are: |
04 |
1. SAPI Deactivate is called from two places: module init and request shutdown |
05 |
2. When the first call occurs and the request is not set up, flush fails on |
08 |
if(SG(sapi_started)) { |
09 |
sapi_cgibin_flush(SG(server_context)); |
5. sapi_cgibin_ub_write, 這個hanlder告訴了Zend,如何輸出數據,對於mod_php來講,這個函數提供了一個向response數據寫的接口,而對於CGI來講,只是簡單的寫到stdout:
01 |
staticinlinesize_tsapi_cgibin_single_write(constchar*str, uint str_length TSRMLS_DC) |
03 |
#ifdef PHP_WRITE_STDOUT |
10 |
if(fcgi_is_fastcgi()) { |
11 |
fcgi_request *request = (fcgi_request*) SG(server_context); |
12 |
longret = fcgi_write(request, FCGI_STDOUT, str, str_length); |
19 |
#ifdef PHP_WRITE_STDOUT |
20 |
ret = write(STDOUT_FILENO, str, str_length); |
24 |
ret =fwrite(str, 1, MIN(str_length, 16384), stdout); |
29 |
staticintsapi_cgibin_ub_write(constchar*str, uint str_length TSRMLS_DC) |
32 |
uint remaining = str_length; |
35 |
while(remaining > 0) { |
36 |
ret = sapi_cgibin_single_write(ptr, remaining TSRMLS_CC); |
38 |
php_handle_aborted_connection(); |
39 |
returnstr_length - remaining; |
把真正的寫的邏輯剝離出來,就是爲了簡單實現兼容fastcgi的寫方式。
6. sapi_cgibin_flush, 這個是提供給zend的刷新緩存的函數句柄,對於CGI來講,只是簡單的調用系統提供的fflush;
7.NULL, 這部分用來讓Zend能夠驗證一個要執行腳本文件的state,從而判斷文件是否據有執行權限等等,CGI沒有提供。
8. sapi_cgibin_getenv, 爲Zend提供了一個根據name來查找環境變量的接口,對於mod_php5來講,當咱們在腳本中調用getenv的時候,就會間接的調用這個句柄。而 對於CGI來講,由於他的運行機制和CLI很相似,直接調用父級是Shell, 因此,只是簡單的調用了系統提供的genenv:
01 |
staticchar*sapi_cgibin_getenv(char*name,size_tname_len TSRMLS_DC) |
04 |
/* when php is started by mod_fastcgi, no regular environment |
05 |
is provided to PHP. It is always sent to PHP at the start |
06 |
of a request. So we have to do our own lookup to get env |
07 |
vars. This could probably be faster somehow. */ |
08 |
if(fcgi_is_fastcgi()) { |
09 |
fcgi_request *request = (fcgi_request*) SG(server_context); |
10 |
returnfcgi_getenv(request, name, name_len); |
13 |
/* if cgi, or fastcgi and not found in fcgi env |
14 |
check the regular environment */ |
9. php_error, 錯誤處理函數, 到這裏,說幾句題外話,上次看到php maillist 提到的使得PHP的錯誤處理機制徹底OO化, 也就是,改寫這個函數句柄,使得每當有錯誤發生的時候,都throw一個異常。而CGI只是簡單的調用了PHP提供的錯誤處理函數。
10. 這個函數會在咱們調用PHP的header()函數的時候被調用,對於CGI來講,不提供。
11. sapi_cgi_send_headers, 這個函數會在要真正發送header的時候被調用,通常來講,就是當有任何的輸出要發送以前:
01 |
staticintsapi_cgi_send_headers(sapi_headers_struct *sapi_headers TSRMLS_DC) |
03 |
charbuf[SAPI_CGI_MAX_HEADER_LENGTH]; |
04 |
sapi_header_struct *h; |
05 |
zend_llist_position pos; |
07 |
if(SG(request_info).no_headers == 1) { |
08 |
return SAPI_HEADER_SENT_SUCCESSFULLY; |
11 |
if(cgi_nph || SG(sapi_headers).http_response_code != 200) |
15 |
if(rfc2616_headers && SG(sapi_headers).http_status_line) { |
16 |
len = snprintf(buf, SAPI_CGI_MAX_HEADER_LENGTH, |
17 |
"%s\r\n", SG(sapi_headers).http_status_line); |
19 |
if(len > SAPI_CGI_MAX_HEADER_LENGTH) { |
20 |
len = SAPI_CGI_MAX_HEADER_LENGTH; |
24 |
len =sprintf(buf,"Status: %d\r\n", SG(sapi_headers).http_response_code); |
30 |
h = (sapi_header_struct*)zend_llist_get_first_ex(&sapi_headers->headers, &pos); |
32 |
/* prevent CRLFCRLF */ |
34 |
PHPWRITE_H(h->header, h->header_len); |
35 |
PHPWRITE_H("\r\n", 2); |
37 |
h = (sapi_header_struct*)zend_llist_get_next_ex(&sapi_headers->headers, &pos); |
39 |
PHPWRITE_H("\r\n", 2); |
41 |
returnSAPI_HEADER_SENT_SUCCESSFULLY; |
12. NULL, 這個用來單獨發送每個header, CGI沒有提供
13. sapi_cgi_read_post, 這個句柄指明瞭如何獲取POST的數據,若是作過CGI編程的話,咱們就知道CGI是從stdin中讀取POST DATA的:
01 |
staticintsapi_cgi_read_post(char*buffer, uint count_bytes TSRMLS_DC) |
03 |
uint read_bytes=0, tmp_read_bytes; |
08 |
count_bytes = MIN(count_bytes, (uint) SG(request_info).content_length - SG(read_post_bytes)); |
09 |
while(read_bytes < count_bytes) { |
11 |
if(fcgi_is_fastcgi()) { |
12 |
fcgi_request *request = (fcgi_request*) SG(server_context); |
13 |
tmp_read_bytes = fcgi_read(request, pos, count_bytes - read_bytes); |
14 |
pos += tmp_read_bytes; |
16 |
tmp_read_bytes = read(0, buffer + read_bytes, count_bytes - read_bytes); |
19 |
tmp_read_bytes = read(0, buffer + read_bytes, count_bytes - read_bytes); |
22 |
if(tmp_read_bytes <= 0) { |
25 |
read_bytes += tmp_read_bytes; |
14. sapi_cgi_read_cookies, 這個和上面的函數同樣,只不過是去獲取cookie值:
1 |
staticchar*sapi_cgi_read_cookies(TSRMLS_D) |
3 |
returnsapi_cgibin_getenv((char*)"HTTP_COOKIE",sizeof("HTTP_COOKIE")-1 TSRMLS_CC); |
15. sapi_cgi_register_variables, 這個函數給了一個接口,用以給$_SERVER變量中添加變量,對於CGI來講,註冊了一個PHP_SELF,這樣咱們就能夠在腳本中訪 問$_SERVER['PHP_SELF']來獲取本次的request_uri:
1 |
staticvoidsapi_cgi_register_variables(zval *track_vars_array TSRMLS_DC) |
3 |
/* In CGI mode, we consider the environment to be a part of the server |
6 |
php_import_environment_variables(track_vars_array TSRMLS_CC); |
7 |
/* Build the special-case PHP_SELF variable for the CGI version */ |
8 |
php_register_variable("PHP_SELF", (SG(request_info).request_uri ? SG(request_info).request_uri :""), track_vars_array TSRMLS_CC); |
16. sapi_cgi_log_message ,用來輸出錯誤信息,對於CGI來講,只是簡單的輸出到stderr:
01 |
staticvoidsapi_cgi_log_message(char*message) |
04 |
if(fcgi_is_fastcgi() && fcgi_logging) { |
05 |
fcgi_request *request; |
08 |
request = (fcgi_request*) SG(server_context); |
10 |
intlen =strlen(message); |
11 |
char*buf =malloc(len+2); |
13 |
memcpy(buf, message, len); |
14 |
memcpy(buf + len,"\n",sizeof("\n")); |
15 |
fcgi_write(request, FCGI_STDERR, buf, len+1); |
18 |
fprintf(stderr,"%s\n", message); |
20 |
/* ignore return code */ |
22 |
#endif /* PHP_FASTCGI */ |
23 |
fprintf(stderr,"%s\n", message); |
通過分析,咱們已經瞭解了一個SAPI是如何實現的了, 分析過CGI之後,咱們也就能夠想象mod_php, embed等SAPI的實現機制。
一次請求的開始與結束
PHP開始執行之後會通過兩個主要的階段:
開始階段有兩個過程:
第一個過程是模塊初始化階段(MINIT), 在整個SAPI生命週期內(例如Apache啓動之後的整個生命週期內或者命令行程序整個執行過程當中), 該過程只進行一次。
第二個過程是模塊激活階段(RINIT),該過程發生在請求階段, 例如經過url請求某個頁面,則在每次請求以前都會進行模塊激活(RINIT請求開始)。 例如PHP註冊了一些擴展模塊,則在MINIT階段會回調全部模塊的MINIT函數。 模塊在這個階段能夠進行一些初始化工做,例如註冊常量,定義模塊使用的類等等。
模塊在實現時能夠經過以下宏來實現這些回調函數:
1 |
PHP_MINIT_FUNCTION(myphpextension) |
請求到達以後PHP初始化執行腳本的基本環境,例如建立一個執行環境,包括保存PHP運行過程當中變量名稱和值內容的符號表, 以及當前全部的函數以及類等信息的符號表。而後PHP會調用全部模塊的RINIT函數, 在這個階段各個模塊也能夠執行一些相關的操做,模塊的RINIT函數和MINIT回調函數相似:
1 |
PHP_RINIT_FUNCTION(myphpextension) |
4 |
// 隨後在請求結束的時候記錄結束時間。這樣咱們就可以記錄下處理請求所花費的時間了 |
請求處理完後就進入告終束階段,通常腳本執行到末尾或者經過調用exit()或die()函數, PHP都將進入結束階段。和開始階段對應,結束階段也分爲兩個環節,一個在請求結束後停用模塊(RSHUWDOWN,對應RINIT), 一個在SAPI生命週期結束(Web服務器退出或者命令行腳本執行完畢退出)時關閉模塊(MSHUTDOWN,對應MINIT)。
1 |
PHP_RSHUTDOWN_FUNCTION(myphpextension) |
3 |
// 例如記錄請求結束時間,並把相應的信息寫入到日至文件中。 |
一次請求生命週期
咱們從未手動開啓過PHP的相關進程,它是隨着Apache的啓動而運行的。PHP經過mod_php5.so模塊和Apache相連(具體說來是SAPI,即服務器應用程序編程接口)。
PHP總共有三個模塊:內核、Zend引擎、以及擴展層。
- PHP內核用來處理請求、文件流、錯誤處理等相關操做;
- Zend引擎(ZE)用以將源文件轉換成機器語言,而後在虛擬機上運行它;
- 擴展層是一組函數、類庫和流,PHP使用它們來執行一些特定的操做。
好比,咱們須要mysql擴展來鏈接MySQL數據庫; 當ZE執行程序時可能會須要鏈接若干擴展,這時ZE將控制權交給擴展,等處理完特定任務後再返還;最後,ZE將程序運行結果返回給PHP內核,它再將結果傳送給SAPI層,最終輸出到瀏覽器上。
深刻探討
真正的內部運行過程沒有這麼簡單。以上過程只是個簡略版,讓咱們再深刻挖掘一下,看看幕後還發生了些什麼。
Apache啓動後,PHP解釋程序也隨之啓動。PHP的啓動過程有兩步:
- 第一步是初始化一些環境變量,這將在整個SAPI生命週期中發生做用;
- 第二步是生成只針對當前請求的一些變量設置。
PHP啓動第一步
不清楚什麼第一第二步是什麼?別擔憂,咱們接下來詳細討論一下。讓咱們先看看第一步,也是最主要的一步。要記住的是,第一步的操做在任何請求到達以前就發生了。
啓動Apache後,PHP解釋程序也隨之啓動。PHP調用各個擴展的MINIT方法,從而使這些擴展切換到可用狀態。看看php.ini文件裏打開了哪些擴展吧。 MINIT的意思是「模塊初始化」。各個模塊都定義了一組函數、類庫等用以處理其餘請求。
一個典型的MINIT方法以下:
1 |
PHP_MINIT_FUNCTION(extension_name){/* Initialize functions, classes etc */} |
PHP啓動第二步
當一個頁面請求發生時,SAPI層將控制權交給PHP層。因而PHP設置了用於回覆本次請求所需的環境變量。同時,它還創建一個變量表,用來存放執 行過程 中產生的變量名和值。PHP調用各個模塊的RINIT方法,即「請求初始化」。一個經典的例子是Session模塊的RINIT,若是在php.ini中 啓用了Session模塊,那在調用該模塊的RINIT時就會初始化$_SESSION變量,並將相關內容讀入;RINIT方法能夠看做是一個準備過程, 在程序執行之間就會自動啓動。 一個典型的RINIT方法以下:
1 |
PHP_RINIT_FUNCTION(extension_name) {/* Initialize session variables,pre-populate variables, redefine global variables etc */} |
PHP關閉第一步
如同PHP啓動同樣,PHP的關閉也分兩步。一旦頁面執行完畢(不管是執行到了文件末尾仍是用exit或die函數停止),PHP就會啓動清理程 序。它會按順序調用各個模塊的RSHUTDOWN方法。 RSHUTDOWN用以清除程序運行時產生的符號表,也就是對每一個變量調用unset函數。
一個典型的RSHUTDOWN方法以下:
1 |
PHP_RSHUTDOWN_FUNCTION(extension_name) {/* Do memory management, unset all variables used in the last PHP call etc */} |
PHP關閉第二步
最後,全部的請求都已處理完畢,SAPI也準備關閉了,PHP開始執行第二步:PHP調用每一個擴展的MSHUTDOWN方法,這是各個模塊最後一次釋放內存的機會。
一個典型的RSHUTDOWN方法以下:
1 |
PHP_MSHUTDOWN_FUNCTION(extension_name) {/* Free handlers and persistent memory etc */} |
這樣,整個PHP生命週期就結束了。要注意的是,只有在服務器沒有請求的狀況下才會執行「啓動第一步」和「關閉第二步」。
單進程SAPI生命週期
CLI/CGI模式的PHP屬於單進程的SAPI模式。這類的請求在處理一次請求後就關閉。也就是隻會通過以下幾個環節: 開始 - 請求開始 - 請求關閉 - 結束 SAPI接口實現就完成了其生命週期。
單進程多請求則以下圖所示:
多進程的SAPI生命週期
一般PHP是編譯爲apache的一個模塊來處理PHP請求。Apache通常會採用多進程模式, Apache啓動後會fork出多個子進程,每一個進程的內存空間獨立,每一個子進程都會通過開始和結束環節, 不過每一個進程的開始階段只在進程fork出來以來後進行,在整個進程的生命週期內可能會處理多個請求。 只有在Apache關閉或者進程被結束以後纔會進行關閉階段,在這兩個階段之間會隨着每一個請求重複請求開始-請求關閉的環節。
多進程SAPI生命週期
多線程的SAPI生命週期
多線程模式和多進程中的某個進程相似,不一樣的是在整個進程的生命週期內會並行的重複着 請求開始-請求關閉的環節。
多線程SAPI生命週期
Zend引擎
相信不少人都據說過 Zend Engine 這個名詞,也有不少人知道 Zend Engine 就是 PHP 語言的核心,但若要問一句:Zend Engine 到底存在於何處?或者說,Zend Engine 到底是在何時怎麼發揮做用讓 PHP 源碼輸出咱們想要的東西的?
Zend引擎是PHP實現的核心,提供了語言實現上的基礎設施。例如:PHP的語法實現,腳本的編譯運行環境, 擴展機制以及內存管理等,固然這裏的PHP指的是官方的PHP實現(除了官方的實現, 目前比較知名的有facebook的hiphop實現,不過到目前爲止,PHP尚未一個標準的語言規範),而PHP則提供了請求處理和其餘Web服務器 的接口(SAPI)。
要理解 Zend Engine 的做用,就不能不理解爲何會出現,PHP 爲何須要 Zend Engine, Zend Engine 的出現爲 PHP 解決了什麼問題。PHP 發展到 3.0 版本的時候,此時 PHP 已經很普及了。「在 PHP3 的頂峯,Internet 上 10% 的 web 服務器上都安裝了它」,PHP Manual 如是說。普遍的應用必然帶來更高的要求。但此時的 PHP3 卻有些力不從心了,這主要是由於 PHP3 採用的是邊解釋邊執行的運行方式,運行效率很受其影響。其次,代碼總體耦合度比較高,可擴展性也不夠好,不利於應付各類各樣需求。所以,此時在 PHP 界裏已經有點中流砥柱做用的 Zeev Suraski 和 Andi Gutmans 決定重寫代碼以解決這兩個問題。最終他們倆把該項技術的核心引擎命名爲 Zend Engine,Zend 的意思即爲 Zeev + Andi 。
Zend Engine 最主要的特性就是把 PHP 的邊解釋邊執行的運行方式改成先進行預編譯(Compile),而後再執行(Execute)。這二者的分開給 PHP 帶來了革命性的變化:執行效率大幅提升;因爲實行了功能分離,下降了模塊間耦合度,可擴展性也大大加強。此時 PHP 已經能很方便的應付各類各樣的 BT 需求了,而伴隨 PHP 4.4.x ―多是 PHP4 系列的最後一個分支―的發佈,PHP 的大部分開發人員已經將注意力放在了 PHP5 或者 PHP6 上面,之後發佈的基本上就是一些 Bug Fix Release。能夠說第一代的 Zend Engine 是已經在站最後一班崗了。
2004 年 7 月,PHP 5 發佈,支持 PHP5 的是 Zend Engine 2.0 版本。這個版本主要是對 PHP 的 OO 功能進行了改進(我沒有提集成 SQLite、PDO 等特性是由於咱們如今談的主要是 Zend Engine 而非 PHP)。核心執行方式(非 OO 部分)較PHP4 的1.0 版本變更不大,因此 PHP5 純粹的執行速度相對於 PHP4 沒有大的提升。而預計將於本月中旬發佈的 PHP 5.1 版本則會攜帶 Zend Engine 2.1 版本,這個版本將提供新的執行方式,執行速度也會快上許多,至少要比 PHP5.0 相對於 PHP4.x 的差異要大不少,因此,PHP 5.1 將會是一個很了很使人期待的版本。
但並不是 PHP5 系列的 Zend Engine 2 就天衣無縫了。前面已經提到過,Zend Engine 將代碼分紅編譯和執行兩大部分。通常狀況下,咱們的代碼完成之後就不多再去改變了。但執行時 PHP 卻不得不還得一次又一次的重複編譯,這根本就是毫無必要的。並且一般狀況下,編譯的所花費的時間並不比執行少多少,說是五五開並不爲過,所以這極大的浪費 了機器的 CPU。基於 Zend Engine 3.0 的 PHP6 將試圖解決這個問題。除此以外,目前的 PHP 對多字節的字符處理也是 PHP 的一大體命缺陷。這在人們聯繫日益國際化的今天幾乎是不可忍受的。而無數人在抨擊 PHP 或 比較 ASP 等同類語言時老是不可避免的要提到這一點。同時受到 IBM 方面的壓力,PHP6 也將會把對多字節字符的處理提到首要日程。這在 PHP6 的 Dev 版本中已經獲得體現。
目前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解耦,則在其餘語言中重用這些擴展也將更加容易了。
在PHP的生命週期的各個階段,一些與服務相關的操做都是經過SAPI接口實現。 這些內置實現的物理位置在PHP源碼的SAPI目錄。這個目錄存放了PHP對各個服務器抽象層的代碼, 例如命令行程序的實現,Apache的mod_php模塊實現以及fastcgi的實現等等。
在各個服務器抽象層之間遵照着相同的約定,這裏咱們稱之爲SAPI接口。 每一個SAPI實現都是一個_sapi_module_struct結構體變量。(SAPI接口)。 在PHP的源碼中,當須要調用服務器相關信息時,所有經過SAPI接口中對應方法調用實現, 而這對應的方法在各個服務器抽象層實現時都會有各自的實現。
下面是爲SAPI的簡單示意圖:
以cgi模式和apache2服務器爲例,它們的啓動方法以下:
1 |
cgi_sapi_module.startup(&cgi_sapi_module) // cgi模式 cgi/cgi_main.c文件 |
3 |
apache2_sapi_module.startup(&apache2_sapi_module); |
4 |
// apache2服務器 apache2handler/sapi_apache2.c文件 |
這裏的cgi_sapi_module是sapi_module_struct結構體的靜態變量。 它的startup方法指向php_cgi_startup函數指針。在這個結構體中除了startup函數指針,還有許多其它方法或字段。 其部分定義以下:
01 |
struct_sapi_module_struct { |
03 |
char*pretty_name; // 更好理解的名字(本身翻譯的) |
05 |
int(*startup)(struct_sapi_module_struct *sapi_module); // 啓動函數 |
06 |
int(*shutdown)(struct_sapi_module_struct *sapi_module); // 關閉方法 |
08 |
int(*activate)(TSRMLS_D); // 激活 |
09 |
int(*deactivate)(TSRMLS_D); // 停用 |
11 |
int(*ub_write)(constchar*str, unsignedintstr_length TSRMLS_DC); |
12 |
// 不緩存的寫操做(unbuffered write) |
13 |
void(*flush)(void*server_context); // flush |
14 |
structstat *(*get_stat)(TSRMLS_D); // get uid |
15 |
char*(*getenv)(char*name,size_tname_len TSRMLS_DC);// getenv |
17 |
void(*sapi_error)(inttype,constchar*error_msg, ...); /* error handler */ |
19 |
int(*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op, |
20 |
sapi_headers_struct *sapi_headers TSRMLS_DC); /* header handler */ |
22 |
/* send headers handler */ |
23 |
int(*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC); |
25 |
void(*send_header)(sapi_header_struct *sapi_header, |
26 |
void*server_context TSRMLS_DC); /* send header handler */ |
28 |
int(*read_post)(char*buffer, uint count_bytes TSRMLS_DC);/* read POST data */ |
29 |
char*(*read_cookies)(TSRMLS_D); /* read Cookies */ |
31 |
/* register server variables */ |
32 |
void(*register_server_variables)(zval *track_vars_array TSRMLS_DC); |
34 |
void(*log_message)(char*message); /* Log message */ |
35 |
time_t(*get_request_time)(TSRMLS_D); /* Request Time */ |
36 |
void(*terminate_process)(TSRMLS_D); /* Child Terminate */ |
38 |
char*php_ini_path_override; // 覆蓋的ini路徑 |
以上的這些結構在各服務器的接口實現中都有定義。如Apache2的定義:
1 |
staticsapi_module_struct apache2_sapi_module = { |
5 |
php_apache2_startup, /* startup */ |
6 |
php_module_shutdown_wrapper, /* shutdown */ |
目前PHP內置的不少SAPI實現都已再也不維護或者變的有些非主流了,PHP社區目前正在考慮將一些SAPI移出代碼庫。 社區對不少功能的考慮是除非真的很是必要,或者某些功能已近很是通用了,不然就在PECL庫中, 例如很是流行的APC緩存擴展將進入核心代碼庫中。
整個SAPI相似於一個面向對象中的模板方法模式的應用。 SAPI.c和SAPI.h文件所包含的一些函數就是模板方法模式中的抽象模板, 各個服務器對於sapi_module的定義及相關實現則是一個個具體的模板。
這樣的結構在PHP的源碼中有多處使用, 好比在PHP擴展開發中,每一個擴展都須要定義一個zend_module_entry結構體。 這個結構體的做用與sapi_module_struct結構體相似,都是一個相似模板方法模式的應用。 在PHP的生命週期中若是須要調用某個擴展,其調用的方法都是zend_module_entry結構體中指定的方法, 如在上一小節中提到的在執行各個擴展的請求初始化時,都是統一調用request_startup_func方法, 而在每一個擴展的定義時,都經過宏PHP_RINIT指定request_startup_func對應的函數。 以VLD擴展爲例:其請求初始化爲PHP_RINIT(vld),與之對應在擴展中須要有這個函數的實現:
1 |
PHP_RINIT_FUNCTION(vld) { |
因此, 咱們在寫擴展時也須要實現擴展的這些接口,一樣,當實現各服務器接口時也須要實現其對應的SAPI。
Apache模塊介紹
Apache概述
Apache是目前世界上使用最爲普遍的一種Web Server,它以跨平臺、高效和穩定而聞名。按照去年官方統計的數據,Apache服務器的裝機量佔該市場60%以上的份額。尤爲是在 X(Unix/Linux)平臺上,Apache是最多見的選擇。其它的Web Server產品,好比IIS,只能運行在Windows平臺上,是基於微軟.Net架構技術的不二選擇。
Apache支持許多特性,大部分經過模塊擴展實現。常見的模塊包括mod_auth(權限驗證)、mod_ssl(SSL和TLS支持) mod_rewrite(URL重寫)等。一些通用的語言也支持以Apache模塊的方式與Apache集成。 如Perl,Python,Tcl,和PHP等。
Apache並非沒有缺點,它最爲詬病的一點就是變得愈來愈重,被廣泛認爲是重量級的WebServer。因此,近年來又涌現出了不少輕量級的替 代產品,好比lighttpd,nginx等等,這些WebServer的優勢是運行效率很高,但缺點也很明顯,成熟度每每要低於Apache,一般只能 用於某些特定場合。
Apache組件邏輯圖
Apache是基於模塊化設計的,整體上看起來代碼的可讀性高於php的代碼,它的核心代碼並很少,大多數的功能都被分散到各個模塊中,各個模塊在 系統啓動的時候按需載入。你若是想要閱讀Apache的源代碼,建議你直接從main.c文件讀起,系統最主要的處理邏輯都包含在裏面。
MPM(Multi -Processing Modules,多重處理模塊)是Apache的核心組件之一,Apache經過MPM來使用操做系 統的資源,對進程和線程池進行管理。Apache爲了可以得到最好的運行性能,針對不一樣的平臺(Unix/Linux、Window)作了優化,爲不一樣的 平臺提供了不一樣的MPM,用戶能夠根據實際狀況進行選擇,其中最常使用的MPM有prefork和worker兩種。至於您的服務器正以哪一種方式運行,取 決於安裝Apache過程當中指定的MPM編譯參數,在X系統上默認的編譯參數爲prefork。因爲大多數的Unix都不支持真正的線程,因此採用了預派 生子進程(prefork)方式,象Windows或者Solaris這些支持線程的平臺,基於多進程多線程混合的worker模式是一種不錯的選擇。對 此感興趣的同窗能夠閱讀有關資料,此處再也不多講。Apache中還有一個重要的組件就是APR(Apache portable Runtime Library),即Apache可移植運行庫,它是一個對操做系統調用的抽象庫,用來實現Apache內部組件對操做系統的使用,提升系統的可移植性。 Apache對於php的解析,就是經過衆多Module中的php Module來完成的。
Apache的邏輯構成以及與操做系統的關係
PHP與Apache
當PHP須要在Apache服務器下運行時,通常來講,它能夠mod_php5模塊的形式集成, 此時mod_php5模塊的做用是接收Apache傳遞過來的PHP文件請求,並處理這些請求, 而後將處理後的結果返回給Apache。若是咱們在Apache啓動前在其配置文件中配置好了PHP模塊(mod_php5), PHP模塊經過註冊apache2的ap_hook_post_config掛鉤,在Apache啓動的時候啓動此模塊以接受PHP文件的請求。
除了這種啓動時的加載方式,Apache的模塊能夠在運行的時候動態裝載, 這意味着對服務器能夠進行功能擴展而不須要從新對源代碼進行編譯,甚至根本不須要中止服務器。 咱們所須要作的僅僅是給服務器發送信號HUP或者AP_SIG_GRACEFUL通知服務器從新載入模塊。 可是在動態加載以前,咱們須要將模塊編譯成爲動態連接庫。此時的動態加載就是加載動態連接庫。 Apache中對動態連接庫的處理是經過模塊mod_so來完成的,所以mod_so模塊不能被動態加載, 它只能被靜態編譯進Apache的核心。這意味着它是隨着Apache一塊兒啓動的。
Apache是如何加載模塊的呢?咱們之前面提到的mod_php5模塊爲例。 首先咱們須要在Apache的配置文件httpd.conf中添加一行:
1 |
LoadModule php5_module modules/mod_php5.so |
這裏咱們使用了LoadModule命令,該命令的第一個參數是模塊的名稱,名稱能夠在模塊實現的源碼中找到。 第二個選項是該模塊所處的路徑。若是須要在服務器運行時加載模塊, 能夠經過發送信號HUP或者AP_SIG_GRACEFUL給服務器,一旦接受到該信號,Apache將從新裝載模塊, 而不須要從新啓動服務器。
在配置文件中添加了所上所示的指令後,Apache在加載模塊時會根據模塊名查找模塊並加載, 對於每個模塊,Apache必須保證其文件名是以「mod_」開始的,如PHP的mod_php5.c。 若是命名格式不對,Apache將認爲此模塊不合法。Apache的每個模塊都是以module結構體的形式存在, module結構的name屬性在最後是經過宏STANDARD20_MODULE_STUFF以__FILE__體現。 關於這點能夠在後面介紹mod_php5模塊時有看到。這也就決定了咱們的文件名和模塊名是相同的。 經過以前指令中指定的路徑找到相關的動態連接庫文件後,Apache經過內部的函數獲取動態連接庫中的內容, 並將模塊的內容加載到內存中的指定變量中。
在真正激活模塊以前,Apache會檢查所加載的模塊是否爲真正的Apache模塊, 這個檢測是經過檢查module結構體中的magic字段實現的。 而magic字段是經過宏STANDARD20_MODULE_STUFF體現,在這個宏中magic的值爲MODULE_MAGIC_COOKIE, MODULE_MAGIC_COOKIE定義以下:
1 |
#define MODULE_MAGIC_COOKIE 0x41503232UL /* "AP22" */ |
最後Apache會調用相關函數(ap_add_loaded_module)將模塊激活, 此處的激活就是將模塊放入相應的鏈表中(ap_top_modules鏈表: ap_top_modules鏈表用來保存Apache中全部的被激活的模塊,包括默認的激活模塊和激活的第三方模塊。)
經過mod_php5支持PHP
Apache對PHP的支持是經過Apache的模塊mod_php5來支持的。若是但願Apache支持PHP的話,在./configure步 驟須要指定--with-apxs2=/usr/local/apache2/bin/apxs 表示告訴編譯器經過Apache的mod_php5 /apxs來提供對PHP5的解析。
在最後一步make install的時候咱們會看到將動態連接庫libphp5.so(Apache模塊)拷貝到apache2的安裝目錄的modules目錄下,而且還需 要在httpd.conf配置文件中添加LoadModule語句來動態將libphp5.so 模塊加載進來,從而實現Apache對php的支持。
因爲該模式實在太經典了,所以這裏關於安裝部分不許備詳述了,相對來講比較簡單。咱們知道nginx通常包括兩個用途HTTP Server和Reverse Proxy Server(反向代理服務器)。在前端能夠部署nginx做爲reverse proxy server,後端佈置多個Apache來實現機羣系統server cluster架構的。
所以,實際生產中,咱們仍舊可以保留Apache+mod_php5的經典App Server,而僅僅使用nginx來當作前端的reverse proxy server來實現代理和負載均衡。 所以,建議nginx(1個或者多個)+多個apache的架構繼續使用下去。
Apache2的mod_php5模塊包括sapi/apache2handler和sapi/apache2filter兩個目錄 在apache2_handle/mod_php5.c文件中,模塊定義的相關代碼以下:
01 |
AP_MODULE_DECLARE_DATA module php5_module = { |
02 |
STANDARD20_MODULE_STUFF, |
03 |
/* 宏,包括版本,小版本,模塊索引,模塊名,下一個模塊指針等信息,其中模塊名以__FILE__體現 */ |
04 |
create_php_config, /* create per-directory config structure */ |
05 |
merge_php_config, /* merge per-directory config structures */ |
06 |
NULL, /* create per-server config structure */ |
07 |
NULL, /* merge per-server config structures */ |
08 |
php_dir_cmds, /* 模塊定義的全部的指令 */ |
10 |
/* 註冊鉤子,此函數經過ap_hoo_開頭的函數在一次請求處理過程當中對於指定的步驟註冊鉤子 */ |
它所對應的是Apache的module結構,module的結構定義以下:
01 |
typedefstructmodule_struct module; |
07 |
void*dynamic_load_handle; |
08 |
structmodule_struct *next; |
10 |
void(*rewrite_args) (process_rec *process); |
11 |
void*(*create_dir_config) (apr_pool_t *p,char*dir); |
12 |
void*(*merge_dir_config) (apr_pool_t *p,void*base_conf,void*new_conf); |
13 |
void*(*create_server_config) (apr_pool_t *p, server_rec *s); |
14 |
void*(*merge_server_config) (apr_pool_t *p,void*base_conf,void*new_conf); |
15 |
constcommand_rec *cmds; |
16 |
void(*register_hooks) (apr_pool_t *p); |
上面的模塊結構與咱們在mod_php5.c中所看到的結構有一點不一樣,這是因爲STANDARD20_MODULE_STUFF的緣由, 這個宏它包含了前面8個字段的定義。STANDARD20_MODULE_STUFF宏的定義以下:
1 |
/** Use this in all standard modules */ |
2 |
#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \ |
3 |
MODULE_MAGIC_NUMBER_MINOR, \ |
9 |
NULL /* rewrite args spot */ |
在php5_module定義的結構中,php_dir_cmds是模塊定義的全部的指令集合,其定義的內容以下:
01 |
constcommand_rec php_dir_cmds[] = |
03 |
AP_INIT_TAKE2("php_value", php_apache_value_handler, NULL, |
04 |
OR_OPTIONS,"PHP Value Modifier"), |
05 |
AP_INIT_TAKE2("php_flag", php_apache_flag_handler, NULL, |
06 |
OR_OPTIONS,"PHP Flag Modifier"), |
07 |
AP_INIT_TAKE2("php_admin_value", php_apache_admin_value_handler, |
08 |
NULL, ACCESS_CONF|RSRC_CONF,"PHP Value Modifier (Admin)"), |
09 |
AP_INIT_TAKE2("php_admin_flag", php_apache_admin_flag_handler, |
10 |
NULL, ACCESS_CONF|RSRC_CONF,"PHP Flag Modifier (Admin)"), |
11 |
AP_INIT_TAKE1("PHPINIDir", php_apache_phpini_set, NULL, |
12 |
RSRC_CONF,"Directory containing the php.ini file"), |
這是mod_php5模塊定義的指令表。它其實是一個command_rec結構的數組。 當Apache遇到指令的時候將逐一遍歷各個模塊中的指令表,查找是否有哪一個模塊可以處理該指令, 若是找到,則調用相應的處理函數,若是全部指令表中的模塊都不能處理該指令,那麼將報錯。 如上可見,mod_php5模塊僅提供php_value等5個指令。
php_ap2_register_hook函數的定義以下:
1 |
voidphp_ap2_register_hook(apr_pool_t *p) |
3 |
ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE); |
4 |
ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE); |
5 |
ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE); |
6 |
ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE); |
以上代碼聲明瞭pre_config,post_config,handler和child_init 4個掛鉤以及對應的處理函數。 其中pre_config,post_config,child_init是啓動掛鉤,它們在服務器啓動時調用。 handler掛鉤是請求掛鉤,它在服務器處理請求時調用。其中在post_config掛鉤中啓動php。 它經過php_apache_server_startup函數實現。php_apache_server_startup函數經過調用 sapi_startup啓動sapi, 並經過調用php_apache2_startup來註冊sapi module struct(此結構在本節開頭中有說明), 最後調用php_module_startup來初始化PHP, 其中又會初始化ZEND引擎,以及填充zend_module_struct中 的treat_data成員(經過php_startup_sapi_content_types)等。
到這裏,咱們知道了Apache加載mod_php5模塊的整個過程,但是這個過程與咱們的SAPI有什麼關係呢? mod_php5也定義了屬於Apache的sapi_module_struct結構:
01 |
staticsapi_module_struct apache2_sapi_module = { |
05 |
php_apache2_startup, /* startup */ |
06 |
php_module_shutdown_wrapper, /* shutdown */ |
09 |
NULL, /* deactivate */ |
11 |
php_apache_sapi_ub_write, /* unbuffered write */ |
12 |
php_apache_sapi_flush, /* flush */ |
13 |
php_apache_sapi_get_stat, /* get uid */ |
14 |
php_apache_sapi_getenv, /* getenv */ |
16 |
php_error, /* error handler */ |
18 |
php_apache_sapi_header_handler, /* header handler */ |
19 |
php_apache_sapi_send_headers, /* send headers handler */ |
20 |
NULL, /* send header handler */ |
22 |
php_apache_sapi_read_post, /* read POST data */ |
23 |
php_apache_sapi_read_cookies, /* read Cookies */ |
25 |
php_apache_sapi_register_variables, |
26 |
php_apache_sapi_log_message, /* Log message */ |
27 |
php_apache_sapi_get_request_time, /* Request Time */ |
28 |
NULL, /* Child Terminate */ |
30 |
STANDARD_SAPI_MODULE_PROPERTIES |
這些方法都專屬於Apache服務器。以讀取cookie爲例,當咱們在Apache服務器環境下,在PHP中調用讀取Cookie時, 最終獲取的數據的位置是在激活SAPI時。它所調用的方法是read_cookies。
1 |
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C); |
對於每個服務器在加載時,咱們都指定了sapi_module,而Apache的sapi_module是 apache2_sapi_module。 其中對應read_cookies方法的是php_apache_sapi_read_cookies函數。 這也是定義SAPI結構的理由:統一接口,面向接口的編程,具備更好的擴展性和適應性。
Apache運行與鉤子函數
Apache是目前世界上使用最爲普遍的一種Web Server,它以跨平臺、高效和穩定而聞名。按照去年官方統計的數據,Apache服務器的裝機量佔該市場60%以上的份額。尤爲是在 X(Unix/Linux)平臺上,Apache是最多見的選擇。其它的Web Server產品,好比IIS,只能運行在Windows平臺上,是基於微軟.Net架構技術的不二選擇。
Apache並非沒有缺點,它最爲詬病的一點就是變得愈來愈重,被廣泛認爲是重量級的WebServer。因此,近年來又涌現出了不少輕量級的替 代產品,好比lighttpd,nginx等等,這些WebServer的優勢是運行效率很高,但缺點也很明顯,成熟度每每要低於Apache,一般只能 用於某些特定場合。
Apache的運行過程
Apache的運行分爲啓動階段和運行階段。 在啓動階段,Apache爲了得到系統資源最大的使用權限,將以特權用戶root(*nix系統)或超級管理員 Administrator(Windows系統)完成啓動, 而且整個過程處於一個單進程單線程的環境中。 這個階段包括配置文件解析(如http.conf文件)、模塊加載(如mod_php,mod_perl)和系統資源初始化(例如日誌文件、共享內存段、 數據庫鏈接等)等工做。
Apache的啓動階段執行了大量的初始化操做,而且將許多比較慢或者花費比較高的操做都集中在這個階段完成,以減小了後面處理請求服務的壓力。
在運行階段,Apache主要工做是處理用戶的服務請求。 在這個階段,Apache放棄特權用戶級別,使用普通權限,這主要是基於安全性的考慮,防止因爲代碼的缺陷引發的安全漏洞。 Apache對HTTP的請求能夠分爲鏈接、處理和斷開鏈接三個大的階段。同時也能夠分爲11個小的階段,依次爲: Post-Read-Request,URI Translation,Header Parsing,Access Control,Authentication,Authorization, MIME Type Checking,FixUp,Response,Logging,CleanUp
Apache Hook機制
Apache的Hook機制是指:Apache 容許模塊(包括內部模塊和外部模塊,例如mod_php5.so,mod_perl.so等)將自定義的函數注入到請求處理循環中。換句話說,模塊能夠在 Apache的任何一個處理階段中掛接(Hook)上本身的處理函數,從而參與Apache的請求處理過程。
mod_php5.so/ php5apache2.dll就是將所包含的自定義函數,經過Hook機制注入到Apache中,在Apache處理流程的各個階段負責處理php請求。
關於Hook機制在Windows系統開發也常常遇到,在Windows開發既有系統級的鉤子,又有應用級的鉤子。常見的翻譯軟件(例如金山詞霸等等)的屏幕取詞功能,大多數是經過安裝系統級鉤子函數完成的,將自定義函數替換gdi32.dll中的屏幕輸出的繪製函數。
Apache 服務器的體系結構的最大特色,就是高度模塊化。若是你爲了追求處理效率,能夠把這些dso模塊在apache編譯的時候靜態鏈入,這樣會提升Apache 5%左右的處理性能。
Apache請求處理循環
Apache請求處理循環的11個階段都作了哪些事情呢?
- Post-Read-Request階段。在正常請求處理流程中,這是模塊能夠插入鉤子的第一個階段。對於那些想很早進入處理請求的模塊來講,這個階段能夠被利用。
- URI Translation階段。Apache在本階段的主要工做:將請求的URL映射到本地文件系統。模塊能夠在這階段插入鉤子,執行本身的映射邏輯。mod_alias就是利用這個階段工做的。
- Header Parsing階段。Apache在本階段的主要工做:檢查請求的頭部。因爲模塊能夠在請求處理流程的任何一個點上執行檢查請求頭部的任務,所以這個鉤子不多被使用。mod_setenvif就是利用這個階段工做的。
- Access Control階段。 Apache在本階段的主要工做:根據配置文件檢查是否容許訪問請求的資源。Apache的標準邏輯實現了容許和拒絕指令。mod_authz_host就是利用這個階段工做的。
- Authentication階段。Apache在本階段的主要工做:按照配置文件設定的策略對用戶進行認證,並設定用戶名區域。模塊能夠在這階段插入鉤子,實現一個認證方法。
- Authorization階段。 Apache在本階段的主要工做:根據配置文件檢查是否容許認證過的用戶執行請求的操做。模塊能夠在這階段插入鉤子,實現一個用戶權限管理的方法。
- MIME Type Checking階段。Apache在本階段的主要工做:根據請求資源的MIME類型的相關規則,斷定將要使用的內容處理函數。標準模塊mod_negotiation和mod_mime實現了這個鉤子。
- FixUp階段。這是一個通用的階段,容許模塊在內容生成器以前,運行任何須要的處理流程。和Post_Read_Request相似,這是一個可以捕獲任何信息的鉤子,也是最常使用的鉤子。
- Response階段。Apache在本階段的主要工做:生成返回客戶端的內容,負責給客戶端發送一個恰當的回覆。這個階段是整個處理流程的核心部分。
- Logging階段。Apache在本階段的主要工做:在回覆已經發送給客戶端以後記錄事務。模塊可能修改或者替換Apache的標準日誌記錄。
- CleanUp階段。 Apache在本階段的主要工做:清理本次請求事務處理完成以後遺留的環境,好比文件、目錄的處理或者Socket的關閉等等,這是Apache一次請求處理的最後一個階段。
從PHP源碼目錄結構的介紹以及PHP生命週期可知:嵌入式PHP相似CLI,也是SAPI接口的另外一種實現。 通常狀況下,它的一個請求的生命週期也會和其它的SAPI同樣:模塊初始化=>請求初始化=>處理請求=>關閉請求=>關閉模 塊。 固然,這只是理想狀況。由於特定的應用由本身特殊的需求,只是在處理PHP腳本這個環節基本一致。
對於嵌入式PHP或許咱們瞭解比較少,或者說根本用不到,甚至在網上相關的資料也很少, 例如不少遊戲中使用Lua語言做爲粘合語言,或者做爲擴展遊戲的腳本語言,相似的, 瀏覽器中的Javascript語言就是嵌入在瀏覽器中的。只是目前不多有應用將PHP做爲嵌入語言來使用, PHP的強項目前仍是在Web開發方面。
PHP對於嵌入式PHP的支持以及PHP爲嵌入式提供了哪些接口或功能呢?首先咱們看下所要用到的示例源碼:
01 |
#include <sapi/embed/php_embed.h> |
06 |
zend_module_entry php_mymod_module_entry = { |
07 |
STANDARD_MODULE_HEADER, |
08 |
"mymod",/* extension name */ |
09 |
NULL,/* function entries */ |
16 |
STANDARD_MODULE_PROPERTIES |
19 |
staticvoidstartup_php(void) |
22 |
char*argv[2] = {"embed5", NULL }; |
23 |
php_embed_init(argc, argv PTSRMLS_CC); |
24 |
zend_startup_module(&php_mymod_module_entry); |
26 |
staticvoidexecute_php(char*filename) |
30 |
spprintf(&include_script, 0,"include '%s'", filename); |
31 |
zend_eval_string(include_script, NULL, filename TSRMLS_CC); |
32 |
efree(include_script); |
35 |
intmain(intargc,char*argv[]) |
38 |
printf("Usage: embed4 scriptfile";); |
43 |
php_embed_shutdown(TSRMLS_CC); |
以上的代碼能夠在《Extending and Embedding PHP》在第20章找到(原始代碼有一個符號錯誤,有興趣的童鞋能夠去圍觀下)。 上面的代碼是一個嵌入式PHP運行器(咱們權當其爲運行器吧),在這個運行器上咱們能夠運行PHP代碼。 這段代碼包括了對於PHP嵌入式支持的聲明,啓動嵌入式PHP運行環境,運行PHP代碼,關閉嵌入式PHP運行環境。 下面咱們就這段代碼分析PHP對於嵌入式的支持作了哪些工做。 首先看下第一行:
1 |
#include <sapi/embed/php_embed.h> |
在sapi目錄下的embed目錄是PHP對於嵌入式的抽象層所在。在這裏有咱們所要用到的函數或宏定義。 如示例中所使用的php_embed_init,php_embed_shutdown等函數。
第2到4行:
ZTS是Zend Thread Safety的簡寫,與這個相關的有一個TSRM(線程安全資源管理)的東東,這個後面的章節會有詳細介紹,這裏就再也不做闡述。
第6到17行:
01 |
zend_module_entry php_mymod_module_entry = { |
02 |
STANDARD_MODULE_HEADER, |
03 |
"mymod",/* extension name */ |
04 |
NULL,/* function entries */ |
11 |
STANDARD_MODULE_PROPERTIES |
以上PHP內部的模塊結構聲明,此處對於模塊初始化,請求初始化等函數指針均爲NULL, 也就是模塊在初始化及請求開始結束等事件發生的時候不執行任何操做。 不過這些操做在sapi/embed/php_embed.c文件中的php_embed_shutdown等函數中有體現。 關於模塊結構的定義在zend/zend_modules.h中。
startup_php函數:
1 |
staticvoidstartup_php(void) |
4 |
char*argv[2] = {"embed5", NULL }; |
5 |
php_embed_init(argc, argv PTSRMLS_CC); |
6 |
zend_startup_module(&php_mymod_module_entry); |
這個函數調用了兩個函數php_embed_init和zend_startup_module完成初始化工做。 php_embed_init函數定義在sapi/embed/php_embed.c文件中。它完成了PHP對於嵌入式的初始化支持。 zend_startup_module函數是PHP的內部API函數,它的做用是註冊定義的模塊,這裏是註冊mymod模塊。 這個註冊過程僅僅是將所定義的zend_module_entry結構添加到註冊模塊列表中。
execute_php函數:
1 |
staticvoidexecute_php(char*filename) |
5 |
spprintf(&include_script, 0,"include '%s'", filename); |
6 |
zend_eval_string(include_script, NULL, filename TSRMLS_CC); |
從函數的名稱來看,這個函數的功能是執行PHP代碼的。 它經過調用sprrintf函數構造一個include語句,而後再調用zend_eval_string函數執行這個include語句。 zend_eval_string最終是調用zend_eval_stringl函數,這個函數是流程是一個編譯PHP代碼, 生成zend_op_array類型數據,並執行opcode的過程。 這段程序至關於下面的這段php程序,這段程序能夠用php命令來執行,雖然下面這段程序沒有實際意義, 而經過嵌入式PHP中,你能夠在一個用C實現的系統中嵌入PHP,而後用PHP來實現功能。
2 |
if($argc< 2)die("Usage: embed4 scriptfile"); |
main函數:
01 |
intmain(intargc,char*argv[]) |
04 |
printf("Usage: embed4 scriptfile";); |
09 |
php_embed_shutdown(TSRMLS_CC); |
這個函數是主函數,執行初始化操做,根據輸入的參數執行PHP的include語句,最後執行關閉操做,返回。 其中php_embed_shutdown函數定義在sapi/embed/php_embed.c文件中。它完成了PHP對於嵌入式的關閉操做支持。 包括請求關閉操做,模塊關閉操做等。
以上是使用PHP的嵌入式方式開發的一個簡單的PHP代碼運行器,它的這些調用的方式都基於PHP自己的一些實現, 而針對嵌入式的SAPI定義是很是簡單的,沒有Apache和CGI模式的複雜,或者說是至關簡陋,這也是由其所在環境決定。 在嵌入式的環境下,不少的網絡協議所須要的方法都再也不須要。以下所示,爲嵌入式的模塊定義。
01 |
sapi_module_struct php_embed_module = { |
03 |
"PHP Embedded Library", /* pretty name */ |
05 |
php_embed_startup, /* startup */ |
06 |
php_module_shutdown_wrapper, /* shutdown */ |
09 |
php_embed_deactivate, /* deactivate */ |
11 |
php_embed_ub_write, /* unbuffered write */ |
12 |
php_embed_flush, /* flush */ |
16 |
php_error, /* error handler */ |
18 |
NULL, /* header handler */ |
19 |
NULL, /* send headers handler */ |
20 |
php_embed_send_header, /* send header handler */ |
22 |
NULL, /* read POST data */ |
23 |
php_embed_read_cookies, /* read Cookies */ |
25 |
php_embed_register_variables, /* register server variables */ |
26 |
php_embed_log_message, /* Log message */ |
27 |
NULL, /* Get request time */ |
28 |
NULL, /* Child terminate */ |
30 |
STANDARD_SAPI_MODULE_PROPERTIES |
在這個定義中咱們看到了若干的NULl定義,在前面一小節中說到SAPI時,咱們是以cookie的讀取爲例, 在這裏也有讀取cookie的實現——php_embed_read_cookies函數,可是這個函數的實現是一個空指針NULL。
PHP的FastCGI
CGI全稱是「通用網關接口」(Common Gateway Interface), 它可讓一個客戶端,從網頁瀏覽器向執行在Web服務器上的程序請求數據。 CGI描述了客戶端和這個程序之間傳輸數據的一種標準。 CGI的一個目的是要獨立於任何語言的,因此CGI能夠用任何一種語言編寫,只要這種語言具備標準輸入、輸出和環境變量。 如php,perl,tcl等。
FastCGI是Web服務器和處理程序之間通訊的一種協議, 是CGI的一種改進方案,FastCGI像是一個常駐(long-live)型的CGI, 它能夠一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI最爲人詬病的fork-and-execute模式)。 正是由於他只是一個通訊協議,它還支持分佈式的運算,即 FastCGI 程序能夠在網站服務器之外的主機上執行而且接受來自其它網站服務器來的請求。
FastCGI是語言無關的、可伸縮架構的CGI開放擴展,將CGI解釋器進程保持在內存中,以此得到較高的性能。 CGI程序反覆加載是CGI性能低下的主要緣由,若是CGI程序保持在內存中並接受FastCGI進程管理器調度, 則能夠提供良好的性能、伸縮性、Fail-Over特性等。
通常狀況下,FastCGI的整個工做流程是這樣的:
- Web Server啓動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)
- FastCGI進程管理器自身初始化,啓動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的鏈接。
- 當客戶端請求到達Web Server時,FastCGI進程管理器選擇並鏈接到一個CGI解釋器。 Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。
- FastCGI子進程完成處理後將標準輸出和錯誤信息從同一鏈接返回Web Server。當FastCGI子進程關閉鏈接時, 請求便告處理完成。FastCGI子進程接着等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個鏈接。 在CGI模式中,php-cgi在此便退出了。
PHP的CGI實現了Fastcgi協議,是一個TCP或UDP協議的服務器接受來自Web服務器的請求, 當啓動時建立TCP/UDP協議的服務器的socket監聽,並接收相關請求進行處理。隨後就進入了PHP的生命週期: 模塊初始化,sapi初始化,處理PHP請求,模塊關閉,sapi關閉等就構成了整個CGI的生命週期。
以TCP爲例,在TCP的服務端,通常會執行這樣幾個操做步驟:
- 調用socket函數建立一個TCP用的流式套接字;
- 調用bind函數將服務器的本地地址與前面建立的套接字綁定;
- 調用listen函數將新建立的套接字做爲監聽,等待客戶端發起的鏈接,當客戶端有多個鏈接鏈接到這個套接字時,可能須要排隊處理;
- 服務器進程調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而創建起一個鏈接;
- 當與客戶端建立鏈接後,服務器調用read_stream函數讀取客戶的請求;
- 處理完數據後,服務器調用write函數向客戶端發送應答。
PHP的FastCGI使你的全部php應用軟件經過mod_fastci運行,而不是mod_phpsusexec。FastCGI應用速度很快 是由於他們持久穩定,沒必要對每個請求都啓動和初始化。這使得應用程序的開發成爲可能,不然在CGI範例是不切實際的(例如一個大型的腳本,或者一個須要 鏈接單個或多個數據庫的應用)。
FastCGI的優勢:
- PHP腳本運行速度更快(3到30倍)。PHP解釋程序被載入內存而不用每次須要時從存儲器讀取,極大的提高了依靠腳本運行的站點的性能。
- 須要使用更少的系統資源。因爲服務器不用每次須要時都載入PHP解釋程序,你能夠將站點的傳輸速度提高很高而沒必要增長cpu負擔。
- 不須要對現有的代碼做任何改變。現有的一切都適用於PHP的FastCGI。
可是也會有潛在問題:
- 對全部的子目錄(/home/USERNAME/public_html/php.ini)你只有一個可用的php.ini文件。這是優 化網站代碼所必需的。若是你須要多個php.ini文件以適應不一樣的腳本須要,你能夠在任何子目錄禁用PHP的快速CGI,而其他的地方則繼續有效。若是 你須要這樣作請聯繫support。
- 你對PHP環境作的任何升級(如php.ini文件的改變)都有幾分鐘的延遲。這是由於爲了更快的速度你的php.ini文件已經被載入內存,而不是每次須要時再從存儲器從新讀取。
前面介紹了PHP的生命週期,PHP的SAPI,SAPI處於PHP整個架構較上層,而真正腳本的執行主要由Zend引擎來完成, 這一小節咱們介紹PHP腳本的執行。
目前編程語言能夠分爲兩大類:
- 第一類是像C/C++, .NET, Java之類的編譯型語言, 它們的共性是:運行以前必須對源代碼進行編譯,而後運行編譯後的目標文件。
- 第二類好比PHP, Javascript, Ruby, Python這些解釋型語言, 他們都無需通過編譯便可「運行」。
雖然能夠理解爲直接運行,但它們並非真的直接就被能被機器理解, 機器只能理解機器語言,那這些語言是怎麼被執行的呢, 通常這些語言都須要一個解釋器, 由解釋器來執行這些源碼, 實際上這些語言仍是會通過編譯環節,只不過它們通常會在運行的時候實時進行編譯。爲了效率,並非全部語言在每次執行的時候都會從新編譯一遍, 好比PHP的各類opcode緩存擴展(如APC, xcache, eAccelerator等),好比Python會將編譯的中間文件保存成pyc/pyo文件, 避免每次運行從新進行編譯所帶來的性能損失。
PHP的腳本的執行也須要一個解釋器, 好比命令行下的php程序,或者apache的mod_php模塊等等。 前面提到了PHP的SAPI接口, 下面就以PHP命令行程序爲例解釋PHP腳本是怎麼被執行的。 例如以下的這段PHP腳本:
2 |
$str="Hello, nowamagic!\n"; |
假設上面的代碼保存在名爲hello.php的文件中, 用PHP命令行程序執行這個腳本:
這段代碼的輸出顯然是Hello, nowamagic!, 那麼在執行腳本的時候PHP/Zend都作了些什麼呢? 這些語句是怎麼樣讓php輸出這段話的呢? 下面將一步一步的進行介紹。
程序的執行
- 如上例中, 傳遞給php程序須要執行的文件, php程序完成基本的準備工做後啓動PHP及Zend引擎, 加載註冊的擴展模塊。
- 初始化完成後讀取腳本文件,Zend引擎對腳本文件進行詞法分析,語法分析。而後編譯成opcode執行。 如過安裝了apc之類的opcode緩存, 編譯環節可能會被跳過而直接從緩存中讀取opcode執行。
PHP在讀取到腳本文件後首先對代碼進行詞法分析,PHP的詞法分析器是經過lex生成的, 詞法規則文件在$PHP_SRC/Zend/zend_language_scanner.l, 這一階段lex會會將源代碼按照詞法規則切分一個一個的標記(token)。PHP中提供了一個函數token_get_all(), 該函數接收一個字符串參數, 返回一個按照詞法規則切分好的數組。 例如將上面的php代碼做爲參數傳遞給這個函數:
4 |
$str="Hello, nowamagic\n"; |
8 |
var_dump(token_get_all($code)); |
運行上面的腳本你將會看到一以下的輸出:
05 |
1 => '<?php // 匹配到的字符串 |
25 |
1 => '"Hello, nowamagic |
這也是Zend引擎詞法分析作的事情,將代碼切分爲一個個的標記,而後使用語法分析器(PHP使用bison生成語法分析器, 規則見$PHP_SRC/Zend/zend_language_parser。y), bison根據規則進行相應的處理, 若是代碼找不到匹配的規則,也就是語法錯誤時Zend引擎會中止,並輸出錯誤信息。 好比缺乏括號,或者不符合語法規則的狀況都會在這個環節檢查。 在匹配到相應的語法規則後,Zend引擎還會進行編譯, 將代碼編譯爲opcode, 完成後,Zend引擎會執行這些opcode, 在執行opcode的過程當中還有可能會繼續重複進行編譯-執行, 例如執行eval,include/require等語句, 由於這些語句還會包含或者執行其餘文件或者字符串中的腳本。
例如上例中的echo語句會編譯爲一條ZEND_ECHO指令, 執行過程當中,該指令由C函數zend_print_variable(zval* z)執行,將傳遞進來的字符串打印出來。 爲了方便理解, 本例中省去了一些細節,例如opcode指令和處理函數之間的映射關係等。 後面的章節將會詳細介紹。
若是想直接查看生成的Opcode,可使用php的vld擴展查看。擴展下載地址: http://pecl.php.net/package/vld。Win下須要本身編譯生成dll文件。
有關PHP腳本編譯執行的細節,請閱讀後面有關詞法分析,語法分析及opcode編譯相關內容。未完待續.......