### FastCGI簡介
[CGI](http://zh.wikipedia.org/wiki/CGI)全稱是“通用網關接口”(Common Gateway Interface),它可以讓一個客戶端,從網頁瀏覽器向執行在Web服務器上的程序請求數據。CGI描述了客戶端和這個程序之間傳輸數據的一種標準。CGI的一個目的是要獨立于任何語言的,所以CGI可以用任何一種語言編寫,只要這種語言具有標準輸入、輸出和環境變量。如php,perl,tcl等。
[FastCGI](http://en.wikipedia.org/wiki/FastCGI)是Web服務器和處理程序之間通信的一種[協議](http://andylin02.iteye.com/blog/648412),是CGI的一種改進方案,[FastCGI](http://baike.baidu.com/view/641394.htm)像是一個常駐(long-live)型的CGI,它可以一直執行,在請求到達時不會花費時間去fork一個進程來處理(這是CGI最為人詬病的fork-and-execute模式)。正是因為他只是一個通信協議,它還支持分布式的運算,即 FastCGI 程序可以在網站服務器以外的主機上執行并且接受來自其它網站服務器來的請求。
FastCGI是語言無關的、可伸縮架構的CGI開放擴展,將CGI解釋器進程保持在內存中,以此獲得較高的性能。CGI程序反復加載是CGI性能低下的主要原因,如果CGI程序保持在內存中并接受FastCGI進程管理器調度,則可以提供良好的性能、伸縮性、Fail-Over特性等。
一般情況下,FastCGI的整個工作流程是這樣的:
1. Web Server啟動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)
1. FastCGI進程管理器自身初始化,啟動多個CGI解釋器進程(可見多個php-cgi)并等待來自Web Server的連接。
1. 當客戶端請求到達Web Server時,FastCGI進程管理器選擇并連接到一個CGI解釋器。 Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。
1. FastCGI子進程完成處理后將標準輸出和錯誤信息從同一連接返回Web Server。當FastCGI子進程關閉連接時, 請求便告處理完成。FastCGI子進程接著等待并處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。 在CGI模式中,php-cgi在此便退出了。
### PHP中的CGI實現
PHP的CGI實現了Fastcgi協議,是一個TCP或UDP協議的服務器接受來自Web服務器的請求,當啟動時創建TCP/UDP協議的服務器的socket監聽,并接收相關請求進行處理。隨后就進入了PHP的生命周期:模塊初始化,sapi初始化,處理PHP請求,模塊關閉,sapi關閉等就構成了整個CGI的生命周期。
以TCP為例,在TCP的服務端,一般會執行這樣幾個操作步驟:
1. 調用socket函數創建一個TCP用的流式套接字;
1. 調用bind函數將服務器的本地地址與前面創建的套接字綁定;
1. 調用listen函數將新創建的套接字作為監聽,等待客戶端發起的連接,當客戶端有多個連接連接到這個套接字時,可能需要排隊處理;
1. 服務器進程調用accept函數進入阻塞狀態,直到有客戶進程調用connect函數而建立起一個連接;
1. 當與客戶端創建連接后,服務器調用read_stream函數讀取客戶的請求;
1. 處理完數據后,服務器調用write函數向客戶端發送應答。
TCP上客戶-服務器事務的時序如圖2.6所示:

圖2.6 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監聽,調用fcgi_init初始化
...
}
在fastcgi.c文件中,fcgi_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(); // 生成新的子進程
switch (pid) {
case 0: // 子進程
parent = 0;
?
/* don't catch our signals */
sigaction(SIGTERM, &old_term, 0); // 終止信號
sigaction(SIGQUIT, &old_quit, 0); // 終端退出符
sigaction(SIGINT, &old_int, 0); // 終端中斷符
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版本的讀操作
... // 省略
} 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函數定義在fastcgi.c文件中,其代碼如下:
int fcgi_finish_request(fcgi_request *req, int force_close)
{
int ret = 1;
?
if (req->fd >= 0) {
if (!req->closed) {
ret = fcgi_flush(req, 1);
req->closed = 1;
}
fcgi_close(req, force_close, 1);
}
return ret;
}
如上,當socket處于打開狀態,并且請求未關閉,則會將執行后的結果刷到客戶端,并將請求的關閉設置為真。將數據刷到客戶端的程序調用的是fcgi_flush函數。在此函數中,關鍵是在于答應頭的構造和寫操作。程序的寫操作是調用的safe_write函數,而safe_write函數中對于最終的寫操作針對win和linux環境做了區分,在Win32下,如果是TCP連接則用send函數,如果是非TCP則和非win環境一樣使用write函數。如下代碼:
#ifdef _WIN32
if (!req->tcp) {
ret = write(req->fd, ((char*)buf)+n, count-n);
} else {
ret = send(req->fd, ((char*)buf)+n, count-n, 0);
if (ret <= 0) {
errno = WSAGetLastError();
}
}
#else
ret = write(req->fd, ((char*)buf)+n, count-n);
#endif
在發送了請求的應答后,服務器端將會執行關閉操作,僅限于CGI本身的關閉,程序執行的是fcgi_close函數。fcgi_close函數在前面提的fcgi_finish_request函數中,在請求應答完后執行。同樣,對于win平臺和非win平臺有不同的處理。其中對于非win平臺調用的是write函數。
以上是一個TCP服務器端實現的簡單說明。這只是我們PHP的CGI模式的基礎,在這個基礎上PHP增加了更多的功能。在前面的章節中我們提到了每個SAPI都有一個專屬于它們自己的sapi_module_struct結構:cgi_sapi_module,其代碼定義如下:
/* {{{ sapi_module_struct cgi_sapi_module */
static sapi_module_struct cgi_sapi_module = {
"cgi-fcgi", /* name */
"CGI/FastCGI", /* pretty name */
?
php_cgi_startup, /* startup */
php_module_shutdown_wrapper, /* shutdown */
?
sapi_cgi_activate, /* activate */
sapi_cgi_deactivate, /* deactivate */
?
sapi_cgibin_ub_write, /* unbuffered write */
sapi_cgibin_flush, /* flush */
NULL, /* get uid */
sapi_cgibin_getenv, /* getenv */
?
php_error, /* error handler */
?
NULL, /* header handler */
sapi_cgi_send_headers, /* send headers handler */
NULL, /* send header handler */
?
sapi_cgi_read_post, /* read POST data */
sapi_cgi_read_cookies, /* read Cookies */
?
sapi_cgi_register_variables, /* register server variables */
sapi_cgi_log_message, /* Log message */
NULL, /* Get request time */
NULL, /* Child terminate */
?
STANDARD_SAPI_MODULE_PROPERTIES
};
/* }}} */
同樣,以讀取cookie為例,當我們在CGI環境下,在PHP中調用讀取Cookie時,最終獲取的數據的位置是在激活SAPI時。它所調用的方法是read_cookies。由SAPI實現來實現獲取cookie,這樣各個不同的SAPI就能根據自己的需要來實現一些依賴環境的方法。
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C);
所有使用PHP的場合都需要定義自己的SAPI,例如在第一小節的Apache模塊方式中,sapi_module是apache2_sapi_module,其對應read_cookies方法的是php_apache_sapi_read_cookies函數,而在我們這里,讀取cookie的函數是sapi_cgi_read_cookies。從sapi_module結構可以看出flush對應的是sapi_cli_flush,在win或非win下,flush對應的操作不同,在win下,如果輸出緩存失敗,則會和嵌入式的處理一樣,調用php_handle_aborted_connection進入中斷處理程序,而其它情況則是沒有任何處理程序。這個區別通過cli_win.c中的PHP_CLI_WIN32_NO_CONSOLE控制。
### 參考資料
- http://www.fastcgi.com/drupal/node/2
- http://baike.baidu.com/view/641394.htm
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊