本文鏈接:http://www.orlion.ml/234/
1、在PHP生命周期的各個階段,一些與服務相關的操作都是通過SAPI接口實現。這些內置實現的物理位置在PHP源碼的SAPI目錄。這個目錄存放了PHP對各個服務器抽象層的代碼,例如命令行程序的實現,Apache的mod_php模塊實現以及fastcgi的實現等等
在各個服務器抽象層之間遵守著相同的約定,這裡我們稱之為SAPI接口。每個SAPI實現都是一個_sapi_module_struct結構體變量。(SAPI接口)。在PHP的源碼中,當需要調用服務器相關信息時,全部通過SAPI接口中對應的方法調用實現,而這些方法在各個服務器抽象層實現時都會有各自的實現。由於很多操作的通用性,有很大一部分接口方法使用的是默認方法。下圖為SPAI的簡單示意圖
以cgi模式和apache2服務器為例,它們的啟動方法如下:
cgi_sapi_module.startup(&cgi_sapi_module) // cgi模式 cgi/cgi_main.c文件 apache_sapi_module.startup(&apache_sapi_module); // apache服務器 apache2handler/sapi_apache2.c文件
這裡的cgi_sapi_module是sapi_module_struct結構體的靜態變量。它的startup方法指向php_cgi_startup函數指針。在這個結構體中除了startup函數指針,還有許多其他方法或字段,這些結構在服務器的接口實現中都有定義
整個SAPI類似於一個面向對象中的模板方法模式的應用。SAPI.c和SAPI.h文件所包含的一些函數就是模板方法模式中的抽象模板,各個服務器對於sapi_module的定義及相關實現則是一個個具體的模板
2、Apache模塊
(1)當PHP需要在Apache服務器下運行時,一般來說,它可以mod_php5模塊的形式集成,此時mod_php5模塊的作用是接收Aapche傳遞過來的PHP文件請求,並處理這些請求,然後將處理後的結果返回給Apache。如果我們在Apache啟動前在其配置文件中配置了PHP模塊,PHP模塊通過注冊apache2的ap_hook_post_config掛鉤,在Apache啟動的時候啟動此模塊以接收PHP文件的請求。
除了這種啟動時的加載方式,Apache的模塊可以在運行的時候動態裝載,這意味著對服務器可以進行功能擴展而不需要重新對源代碼進行編譯,甚至不需要重啟服務器。我們所需要做的僅僅是給服務器發送信號HUP或者AP_SIG_GEACEFUL通知服務器重新載入模塊。但是在動態裝載之前我們需要將模塊編譯成為動態鏈接庫。此時的動態加載就是加載動態鏈接庫。Apache中對動態鏈接庫的處理是通過模塊mod_so來完成的,因此mod_so模塊不能被動態加載,它只能本靜態編譯進Apache的核心。這意味著它和Apache一起啟動的。
Apache是如何加載模塊的呢?以mod_php5為例,首先在httpd.conf中添加一行:
LoadModule php5_module modules/mod_php5.so
在配置文件中添加了所示的指令後,Apache在加載模塊時會根據模塊名查找模塊並加載。Apache的每一個模塊都是以module結構體的形式存在,module結構的name屬性在最後是通過宏STANDARD20_MODULE_STUFF以__FILE__體現。通過之前的指令中指定的路徑找到相關的動態鏈接庫文件後,Apache通過內部的函數獲取動態鏈接庫中的內容,並將模塊的內容加載到內存中指定變量中。
在真正激活模塊之前,Apache會檢查所有加載的模塊是否為真正的Apache模塊。最後Apache會調用相關的函數(ap_add_loaded_module)將模塊激活,此處的激活就是將模塊放入相應的鏈表中(ap_top_modules鏈表)
Apache加載的是PHP模塊,那麼這個模塊時怎麼實現的呢?Apache2的mod_php5模塊包括sapi/apache2handler和sapi/apache2filter兩個目錄,在apache2_handle/mod_php5.c文件中,模塊定義的相關代碼如下:
AP_MODULE_DECLARE_DATA module php5_module = { STANDARD20_MODULE_STUFF, /* 宏,包括版本,小版本,模塊索引,模塊名,下一個模塊指針等信息,其中模塊名以__FILE__體現*/ create_php_config, /* create per-directory config structure */ merge_php_config, /* merge per-directory config structures */ NULL, /* create per-server config structure */ NULL, /* merge per-server config structures */ php_dir_cmds, /*模塊定義的所有命令*/ php_ap2_register_hook /*注冊鉤子,此函數通過ap_hoo_開頭的函數在一次處理過程中對於指定的步驟注冊鉤子*/ };
它所對應的是Apache的module結構,module的結構定義如下:
typedef struct module_struct module; struct module_struct { int version; int minor_version; int module_index; const char *name; void *dynamic_load_handle; struct module_struct *next; unsigned long magic; void (*rewrite_args) (process_rec *process); void *(*create_dir_config) (apr_pool_t *p, char *dir); void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf); void *(*create_server_config) (apr_pool_t *p, server_rec *s); void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void *new_conf); const command_rec *cmds; void (*register_hooks) (apr_pool_t *p); }
上面的模塊結構與我們在mod_php5.c中所看到的結構有一點不同,這是由於STANDARD20_MODULE_STUFF的原因,這個宏它包含了前面8個字段的定義。STANDARD20_MODULE_STUFF宏的定義如下:
/** Use this in all standard modules */ #define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \ MODULE_MAGIC_NUMBER_MINOR, \ -1, \ __FILE__, \ NULL, \ NULL, \ MODULE_MAGIC_COOKIE, \ NULL /* rewrite args spot */
在php5_module定義的結構中,php_dir_cmds是模塊定義的所有的指令集合,定義的內容如下:
const command_rec php_dir_cmds[] = { AP_INIT_TAKE2("php_value", php_apache_value_handler, NULL, OR_OPTIONS, "PHP Value Modifier"), AP_INIT_TAKE2("php_flag", php_apache_flag_handler, NULL, OR_OPTIONS, "PHP Flag Modifier"), AP_INIT_TAKE2("php_admin_value", php_apache_admin_value_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Value Modifier (Admin)"), AP_INIT_TAKE2("php_admin_flag", php_apache_admin_flag_handler, NULL, ACCESS_CONF|RSRC_CONF, "PHP Flag Modifier (Admin)"), AP_INIT_TAKE1("PHPINIDir", php_apache_phpini_set, NULL, RSRC_CONF, "Directory containing the php.ini file"), {NULL} };
這是mod_php5模塊定義的指令表。它實際上是一個commond_rec結構的數組。當Apache遇到指令的時候將逐一遍歷各個模塊中的指令表,查找是否有那個模塊能夠處理該指令,如果找到,則調用響應的處理函數,如果所有指令表中的模塊都不能處理該指令,那麼將報錯,如上所見,mod_php5模塊僅提供php_value等5個指令。
php_ap2_register_hook函數的定義如下:
void php_ap2_register_hook(apr_pool_t *p) { ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE); }
以上代碼聲明了pre_config,post_config,handler和child_init4個掛鉤以及對應的處理函數。其中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結構:
static sapi_module_struct apache2_sapi_module = { "apache2handler", "Apache 2.0 Handler", php_apache2_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ NULL, /* activate */ NULL, /* deactivate */ php_apache_sapi_ub_write, /* unbuffered write */ php_apache_sapi_flush, /* flush */ php_apache_sapi_get_stat, /* get uid */ php_apache_sapi_getenv, /* getenv */ php_error, /* error handler */ php_apache_sapi_header_handler, /* header handler */ php_apache_sapi_send_headers, /* send headers handler */ NULL, /* send header handler */ php_apache_sapi_read_post, /* read POST data */ php_apache_sapi_read_cookies, /* read Cookies */ php_apache_sapi_register_variables, php_apache_sapi_log_message, /* Log message */ php_apache_sapi_get_request_time, /* Request Time */ NULL, /* Child Terminate */ STANDARD_SAPI_MODULE_PROPERTIES };
這些方法都屬於Apache服務器,以讀取cookie為例,當我們在Apache服務器環境下,在PHP中調用讀取Cookie時,最終獲取的數據的位置是在激活SAPI時,它所調用的方法是read_cookie。
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);
對於每一個服務器在加載時,我們都指定了sapi_module,而Apache的sapi_module是apache2_sapi_module。其中對應read_cookie的方法是php_apache_sapi_read_cookie函數。這也是定義SAPI結構的理由:統一接口,面向接口編程,具有更好的擴展性和適應性。
(2)Apache的運行過程
Apache的運行包括啟動階段和運行階段,啟動階段Apache以root完成啟動,整個過程處於單進程單線程的環境中,這個階段包括配置文件解析、模塊加載、系統資源初始化(例如日志文件、共享內存段、數據庫連接等)等工作。
在運行階段,Apache主要工作是處理用戶的服務請求,在這個階段Apache以普通用戶運行。主要是安全性考慮,Apache對HTTP的請求可以分為連接、處理和斷開連接三個大的階段。
2、FastCGI
(1)cgi是通用網關接口(Common Gateway Intedface),它可以讓一個客戶端從網頁浏覽器向執行在Web服務器上的程序請求數據。CGI描述了客戶端和這個程序之間傳輸數據的標准。CGI的一個目的是獨立於任何語言,所以CGI可以用任何語言編寫,只要這種語言具有標准輸入、輸出和環境變量。如PHP、perl、tcl等。
FastCGI是Web服務器和處理程序之間通信的一種協議,是CGI的一種改進方案,FastCGI像是一個常駐型的CGI,它可以一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI對位人诟病的fork-and-execute模式)。正是因為它只是一個通信協議,它還支持分布式的運算,即FastCGI程序可以在網站服務器以外的主機上執行並且接受來自其他網站服務器的請求
FastCGI的整個流程是這樣的:
Step1:Web Server啟動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)
Step2:FastCGI進程管理器自身初始化,啟動多個CGI解釋器進程(可見多個php-cgi)並等待來自web server的連接
Step3:當客戶端請求到達Web Server時,FastCGI進程管理器選擇並連接到一個CGI解釋器。Web Server將CGI環境變量和標准輸入發送到FastCGI子進程php-cgi
Step4:FastCGI子進程完成處理後將標准輸出和錯誤新詞從同一連接返回Web Server 當FastCGI子進程關閉連接時,請求便結束。FastCGI子進程接著等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。在CGI模式中,php-cgi在此便退出了。
(2)php中CGI實現
PHP的CGI實現了Fastcgi協議。是一個TCP或UDP協議的服務器接受來自Web服務器的請求,當啟動時創建TCP/UDP協議的服務器的socket監聽,並接受相關請求並進行處理。隨後就進入了PHP的生命周期:模塊初始化,sapi初始化,處理PHP請求,模塊關閉,sapi關閉等 就構成了整個CGI的生命周期。
以TCP為例在,在TCP的服務端,一般會執行這樣幾個步驟:
1、調用socket函數創建一個TCP用的流式套接字;
2、調用bind函數將服務器的本地地址與前面創建的套接字綁定;
3、調用listen函數將新創建的套接字作為監聽,等待客戶端發起的連接,當客戶端有多個連接連接到這個套接字時,可能需要排隊處理;
4、服務器進程調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而建立起一個連接;
5、當與客戶端創建連接後,服務器調用read_stream函數讀取客戶端的請求;
6、處理完數據後,服務器調用write函數向客戶端發送應答
TCP上客戶-服務器事務的時序如圖所示:
php的CGI實現從cgi_main.c文件的main函數開始,在main函數中調用了定義在fastcgi.c文件中的初始化,監聽等函數。對比TCP的流程,我們查看php對TCP協議的實現,雖然php本身也實現了這些流程,但是在main函數中一些過程被封裝成一個函數實現。對應TCP的操作流程,PHP首先會執行創建socket,綁定套接字,創建監聽:
if (bindpath) { fcgi_fd = fcgi_listen(bindpath, 128); // socket˥˦2sfcgi_initɩ Ȑ ... }
在fastcgi.c文件中,fcig_listen函數主要用於創建、綁定socket並開始監聽,它走完了前面所列TCP流程的前三個階段,
if ((listen_socket = socket(sa.sa.sa_family, SOCK_STREAM, 0)) < 0 || ... bind(listen_socket, (struct sockaddr *) &sa, sock_len) < 0 || listen(listen_socket, backlog) < 0) { ... }
當服務端初始化完成後,進程調用accept函數進入阻塞狀態,在main函數中我們看到如下代碼:
while (parent) { do { pid = fork(); // oÒ ȨėJ switch (pid) { case 0: // ȨėJ parent = 0; /* don't catch our signals */ sigaction(SIGTERM, &old_term, 0); // ľâ¯ķ sigaction(SIGQUIT, &old_quit, 0); // ľĿɰ£ƺ sigaction(SIGINT, &old_int, 0); // ľĿKȠƺ break; ... default: /* Fine */ running++; break; } while (parent && (running < children)); ... while (!fastcgi || fcgi_accept_request(&request) >= 0) { SG(server_context) = (void *) &request; init_request_info(TSRMLS_C); CG(interactive) = 0; ... }
如上的代碼是一個生成子進程,並等待用戶請求。在fcgi_accept_request函數中,程序會調用accept函數阻塞新創建的線程。當用戶的請求到達時,fcgi_accept_request函數會判斷是否處理用戶的請求,其中會過濾某些連接請求,忽略受限制客戶的請求,如果程序受理用戶的請求,他將分析請求的信息,將相關的變量寫到對應的變量中。其中在讀取請求內容時調用了safe_read方法。如下所示:main()->fcgi_accept_request()->fcgi_read_request()->safe_read()
static inline ssize_t safe_read(fcgi_request *req, const void *buf, size_t count) { size_t n = 0; do { ... // 省略 對win32的處理 ret = read(req->fd, ((char*)buf)+n, count-n); // 非win版本的讀操作 D ... // 省略 } while (n != count); }
如上對應服務器端讀取用戶的請求數據。
在請求初始化完成,讀取請求完畢後,就該處理請求的PHP文件了。假設此次請求為PHP_MODE_STANDARD則會調用php_execute_script執行PHP文件。在此函數中它先初始化此文件相關的一些內容,然後再調用zend_execute_scripts函數,對PHP文件進行詞法分析和語法分析,生成中間代碼,並執行zend_execute函數,從而執行這些中間代碼。
在處理完用戶的請求後,服務端將返回信息給客戶端,此時在main函數中調用的是fcgi_finish_request(&request , 1);fcgi_finish_request函數定義在fasftcgi.c文件中。
在發送了請求的應答後,服務器端將會執行關閉操作,僅限於CGI本身的關閉,程序執行的是fcgi_close函數。