1.PHP概述
1.1 PHP的歷史發(fā)展
1995年由Lerdorf創(chuàng)建PHP撵枢,高級腳本語言吠架,尤其適合Web開發(fā)芙贫,快速、靈活和實用是PHP最重要的特點傍药。
在1997年磺平,任職于 Technion IIT 公司的兩個以色列程序設計師:Zeev Suraski 和 Andi Gutmans,重寫了PHP的解析器拐辽。
1998年6月正式發(fā)布 PHP 3拣挪。Zeev Suraski 和 Andi Gutmans 在 PHP 3 發(fā)布后開始改寫 PHP 的核心,這個在1999年發(fā)布的解析器稱為 Zend Engine俱诸。
在2000年5月22日菠劝,以Zend Engine 1.0為基礎的PHP 4正式發(fā)布。
2004年7月13日則發(fā)布了PHP 5睁搭,PHP 5則使用了第二代的Zend Engine赶诊。
2015年6月11日,PHP官網(wǎng)發(fā)布消息园骆,正式公開發(fā)布PHP7第一版的alpha版本舔痪。
1.2 特性
PHP 獨特的語法混合了 C、Java锌唾、Perl 以及 PHP 自創(chuàng)新的語法辙喂,豐富的語法支持、同時支持面向?qū)ο箴椤⒚嫦蜻^程巍耗,相比C、Java等語言具有語法簡潔渐排、使用靈活炬太、開發(fā)效率高。
1.3 PHP的相關組成
1.3.1 SAPI
SAPI是PHP的接入層驯耻,它接收用戶的請求亲族,然后調(diào)用PHP內(nèi)核提供的一些接口完成PHP腳本的執(zhí)行炒考,所以嚴格意義上講SAPI并不算PHP內(nèi)核的一部分。PHP本身可以理解為是一個庫函數(shù)霎迫,提供語言的編譯與執(zhí)行服務斋枢,它有標準的輸入、輸出知给。
PHP中常用的SAPI有CLI瓤帚、php-fpm(CGI),CLI是命令行下執(zhí)行PHP腳本的實現(xiàn):bin/php script.php涩赢,它是單進程的戈次,處理模型比較簡單,而php-fpm相對比較復雜筒扒,它實現(xiàn)了網(wǎng)絡處理模塊怯邪,用于與web服務器交互。
1.3.2 Zend引擎
Zend是PHP語言實現(xiàn)的最為重要的部分花墩,是PHP最基礎悬秉、最核心的部分,它的源碼在/Zend目錄下冰蘑,PHP代碼從編譯到執(zhí)行都是由Zend完成的和泌,后面章節(jié)絕大部分的源碼分析都是針對Zend的。Zend整體由兩個部分組成:
- 編譯器: 負責將PHP代碼編譯為抽象語法樹懂缕,然后進一步編譯為可執(zhí)行的opcodes允跑,這個過程相當于GCC的工作,編譯器是一個語言實現(xiàn)的基礎
- 執(zhí)行器: 負責執(zhí)行編譯器輸出的opcodes搪柑,也就是執(zhí)行PHP腳本中編寫的代碼邏輯
2. 執(zhí)行流程
PHP的生命周期
-
php_module_startup() 模塊初始化階段
-
php_request_startup() 請求初始化階段
-
php_execute_script() 執(zhí)行PHP腳本階段
-
php_request_shutdown() 請求結(jié)束階段
-
php_module_shutdown() 模塊關閉階段
3. FPM
3.1 概述
FPM(FastCGI Process Manager)是PHP FastCGI運行模式的一個進程管理器聋丝,核心功能是進程管理,那么它用來管理什么進程呢工碾?這個問題就需要從FastCGI說起了弱睦。
FastCGI是Web服務器(如:Nginx、Apache)和處理程序之間的一種通信協(xié)議渊额,它是與Http類似的一種應用層通信協(xié)議况木,注意:它只是一種協(xié)議!
web服務器來處理http請求旬迹,然后將解析的結(jié)果再通過FastCGI協(xié)議轉(zhuǎn)發(fā)給處理程序火惊,處理程序處理完成后將結(jié)果返回給web服務器,web服務器再返回給用戶奔垦,如下圖所示屹耐。
PHP實現(xiàn)了FastCGI協(xié)議的解析,但是并沒有具體實現(xiàn)網(wǎng)絡處理椿猎,一般的處理模型:多進程惶岭、多線程寿弱。多進程模型通常是主進程只負責管理子進程,而基本的網(wǎng)絡事件由各個子進程處理按灶,nginx症革、fpm就是這種模式。
3.2 基本實現(xiàn)
fpm的實現(xiàn)就是創(chuàng)建一個master進程鸯旁,在master進程中創(chuàng)建并監(jiān)聽socket噪矛,然后fork出多個子進程,這些子進程各自accept請求羡亩。
fpm的子進程子進程在啟動后阻塞在accept上摩疑,同時只能響應一個請求危融,只有把這個請求處理完成后才會accept下一個請求畏铆。(nginx則是一個進程會同時連接多個請求,它是非阻塞的模型吉殃,只處理活躍的套接字)辞居。
fpm的master進程與worker進程之間不會直接進行通信,master通過共享內(nèi)存獲取worker進程的信息蛋勺,比如worker進程當前狀態(tài)瓦灶、已處理請求數(shù)等,當master進程要殺掉一個worker進程時則通過發(fā)送信號的方式通知worker進程抱完。
fpm可以同時監(jiān)聽多個端口贼陶,每個端口對應一個worker pool,而每個pool下對應多個worker進程巧娱,類似nginx中server概念碉怔。
具體實現(xiàn)上worker pool通過fpm_worker_pool_s這個結(jié)構(gòu)表示,多個worker pool組成一個單鏈表:
struct fpm_worker_pool_s {
struct fpm_worker_pool_s *next; //指向下一個worker pool
struct fpm_worker_pool_config_s *config; //conf配置:pm禁添、max_children撮胧、start_servers...
int listening_socket; //監(jiān)聽的套接字
...
//以下這個值用于master定時檢查、記錄worker數(shù)
struct fpm_child_s *children; //當前pool的worker鏈表
int running_children; //當前pool的worker運行總數(shù)
int idle_spawn_rate;
int warn_max_children;
struct fpm_scoreboard_s *scoreboard; //記錄worker的運行信息老翘,比如空閑芹啥、忙碌worker數(shù)
...
}
3.3 FPM的初始化
fpm的啟動流程,從main()函數(shù)開始:
//sapi/fpm/fpm/fpm_main.c
int main(int argc, char *argv[])
{
...
//注冊SAPI:將全局變量sapi_module設置為cgi_sapi_module
sapi_startup(&cgi_sapi_module);
...
//執(zhí)行php_module_starup()
if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
return FPM_EXIT_SOFTWARE;
}
...
//初始化
if(0 > fpm_init(...)){
...
}
...
fpm_is_running = 1;
fcgi_fd = fpm_run(&max_requests);//后面都是worker進程的操作铺峭,master進程不會走到下面
parent = 0;
...
}
fpm_init()主要有以下幾個關鍵操作:
- 1.fpm_conf_init_main():
解析php-fpm.conf配置文件墓怀,分配worker pool內(nèi)存結(jié)構(gòu)并保存到全局變量中:fpm_worker_all_pools,各worker pool配置解析到fpm_worker_pool_s->config中卫键。 - 2.fpm_scoreboard_init_main():
分配用于記錄worker進程運行信息的共享內(nèi)存傀履,按照worker pool的最大worker進程數(shù)分配,每個worker pool分配一個fpm_scoreboard_s結(jié)構(gòu)永罚,pool下對應的每個worker進程分配一個fpm_scoreboard_proc_s結(jié)構(gòu)啤呼,各結(jié)構(gòu)的對應關系如下圖卧秘。
- 3.fpm_signals_init_main(): master的信號處理
static int sp[2];
int fpm_signals_init_main()
{
struct sigaction act;
//創(chuàng)建一個全雙工管道
if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
return -1;
}
//注冊信號處理handler
act.sa_handler = sig_handler;
sigfillset(&act.sa_mask);
if (0 > sigaction(SIGTERM, &act, 0) ||
0 > sigaction(SIGINT, &act, 0) ||
0 > sigaction(SIGUSR1, &act, 0) ||
0 > sigaction(SIGUSR2, &act, 0) ||
0 > sigaction(SIGCHLD, &act, 0) ||
0 > sigaction(SIGQUIT, &act, 0)) {
return -1;
}
return 0;
}
- 4.fpm_sockets_init_main(): 創(chuàng)建每個worker pool的socket套接字。
- 5.fpm_event_init_main():
啟動master的事件管理官扣,fpm實現(xiàn)了一個事件管理器用于管理IO翅敌、定時事件,其中IO事件通過kqueue惕蹄、epoll蚯涮、poll、select等管理卖陵,定時事件就是定時器遭顶,一定時間后觸發(fā)某個事件。
在fpm_init()初始化完成后接下來就是最關鍵的fpm_run()操作了泪蔫,此環(huán)節(jié)將fork子進程棒旗,啟動進程管理器,另外master進程將不會再返回撩荣,只有各worker進程會返回铣揉,也就是說fpm_run()之后的操作均是worker進程的。
int fpm_run(int *max_requests)
{
struct fpm_worker_pool_s *wp;
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
//調(diào)用fpm_children_make() fork子進程
is_parent = fpm_children_create_initial(wp);
if (!is_parent) {
goto run_child;
}
}
//master進程將進入event循環(huán)餐曹,不再往下走
fpm_event_loop(0);
run_child: //只有worker進程會到這里
*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket; //返回監(jiān)聽的套接字
}
在fork后worker進程返回了監(jiān)聽的套接字繼續(xù)main()后面的處理逛拱,而master將永遠阻塞在fpm_event_loop(),接下來分別介紹master台猴、worker進程的后續(xù)操作朽合。
3.4 請求處理
fpm_run()執(zhí)行后將fork出worker進程,worker進程返回main()中繼續(xù)向下執(zhí)行饱狂,后面的流程就是worker進程不斷accept請求曹步,然后執(zhí)行PHP腳本并返回。整體流程如下:
- 1.等待請求: worker進程阻塞在fcgi_accept_request()等待請求嗡官;
- 2.解析請求: fastcgi請求到達后被worker接收箭窜,然后開始接收并解析請求數(shù)據(jù),直到request數(shù)據(jù)完全到達衍腥;
- 3.請求初始化: 執(zhí)行php_request_startup()磺樱,此階段會調(diào)用每個擴展的:PHP_RINIT_FUNCTION();
- 4.編譯婆咸、執(zhí)行: 由php_execute_script()完成PHP腳本的編譯竹捉、執(zhí)行;
- 5.關閉請求: 請求完成后執(zhí)行php_request_shutdown()尚骄,此階段會調(diào)用每個擴展的:PHP_RSHUTDOWN_FUNCTION()块差,然后進入步驟1等待下一個請求。
worker進程一次請求的處理被劃分為5個階段:
- FPM_REQUEST_ACCEPTING: 等待請求階段
- FPM_REQUEST_READING_HEADERS: 讀取fastcgi請求header階段
- FPM_REQUEST_INFO: 獲取請求信息階段,此階段是將請求的method憨闰、query stirng状蜗、request uri等信息保存到各worker進程的fpm_scoreboard_proc_s結(jié)構(gòu)中,此操作需要加鎖鹉动,因為master進程也會操作此結(jié)構(gòu)
- FPM_REQUEST_EXECUTING: 執(zhí)行請求階段
- FPM_REQUEST_END: 沒有使用
- FPM_REQUEST_FINISHED: 請求處理完成
worker處理到各個階段時將會把當前階段更新到fpm_scoreboard_proc_s->request_stage
轧坎,master進程正是通過這個標識判斷worker進程是否空閑的。
3.5 進程管理
這一節(jié)我們來看下master是如何管理worker進程的泽示,首先介紹下三種不同的進程管理方式:
-
static: 這種方式比較簡單缸血,在啟動時master按照
pm.max_children
配置fork出相應數(shù)量的worker進程,即worker進程數(shù)是固定不變的 -
dynamic: 動態(tài)進程管理械筛,首先在fpm啟動時按照
pm.start_servers
初始化一定數(shù)量的worker捎泻,運行期間如果master發(fā)現(xiàn)空閑worker數(shù)低于pm.min_spare_servers
配置數(shù)(表示請求比較多,worker處理不過來了)則會fork worker進程埋哟,但總的worker數(shù)不能超過pm.max_children
笆豁,如果master發(fā)現(xiàn)空閑worker數(shù)超過了pm.max_spare_servers
(表示閑著的worker太多了)則會殺掉一些worker,避免占用過多資源定欧,master通過這4個值來控制worker數(shù) -
ondemand: 這種方式一般很少用渔呵,在啟動時不分配worker進程怒竿,等到有請求了后再通知master進程fork worker進程砍鸠,總的worker數(shù)不超過
pm.max_children
,處理完成后worker進程不會立即退出耕驰,當空閑時間超過pm.process_idle_timeout
后再退出
前面介紹到在fpm_run()
master進程將進入fpm_event_loop()
:
void fpm_event_loop(int err)
{
//創(chuàng)建一個io read的監(jiān)聽事件爷辱,這里監(jiān)聽的就是在fpm_init()階段中通過socketpair()創(chuàng)建管道sp[0]
//當sp[0]可讀時將回調(diào)fpm_got_signal()
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
fpm_event_add(&signal_fd_event, 0);
//如果在php-fpm.conf配置了request_terminate_timeout則啟動心跳檢查
if (fpm_globals.heartbeat > 0) {
fpm_pctl_heartbeat(NULL, 0, NULL);
}
//定時觸發(fā)進程管理
fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);
//進入事件循環(huán),master進程將阻塞在此
while (1) {
...
//等待IO事件
ret = module->wait(fpm_event_queue_fd, timeout);
...
//檢查定時器事件
...
}
}
這就是master整體的處理朦肘,其進程管理主要依賴注冊的幾個事件饭弓,接下來我們詳細分析下這幾個事件的功能。
(1)sp[1]管道可讀事件:
在fpm_init()
階段master曾創(chuàng)建了一個全雙工的管道:sp媒抠,然后在這里創(chuàng)建了一個sp[0]可讀的事件弟断,當sp[0]可讀時將交由fpm_got_signal()
處理,向sp[1]寫數(shù)據(jù)時sp[0]才會可讀趴生,那么什么時機會向sp[1]寫數(shù)據(jù)呢阀趴?前面已經(jīng)提到了:當master收到注冊的那幾種信號時會寫入sp[1]端,這個時候?qū)⒂|發(fā)sp[0]可讀事件苍匆。
這個事件是master用于處理信號的刘急,我們根據(jù)master注冊的信號逐個看下不同用途:
- SIGINT/SIGTERM/SIGQUIT: 退出fpm,在master收到退出信號后將向所有的worker進程發(fā)送退出信號浸踩,然后master退出
- SIGUSR1: 重新加載日志文件叔汁,生產(chǎn)環(huán)境中通常會對日志進行切割,切割后會生成一個新的日志文件,如果fpm不重新加載將無法繼續(xù)寫入日志据块,這個時候就需要向master發(fā)送一個USR1的信號
- SIGUSR2: 重啟fpm码邻,首先master也是會向所有的worker進程發(fā)送退出信號,然后master會調(diào)用execvp()重新啟動fpm另假,最后舊的master退出
- SIGCHLD: 這個信號是子進程退出時操作系統(tǒng)發(fā)送給父進程的冒滩,子進程退出時,內(nèi)核將子進程置為僵尸狀態(tài)浪谴,這個進程稱為僵尸進程开睡,它只保留最小的一些內(nèi)核數(shù)據(jù)結(jié)構(gòu),以便父進程查詢子進程的退出狀態(tài)苟耻,只有當父進程調(diào)用wait或者waitpid函數(shù)查詢子進程退出狀態(tài)后子進程才告終止篇恒,fpm中當worker進程因為異常原因(比如coredump了)退出而非master主動殺掉時master將受到此信號,這個時候父進程將調(diào)用waitpid()查下子進程的退出凶杖,然后檢查下是不是需要重新fork新的worker
具體處理邏輯在fpm_got_signal()
函數(shù)中胁艰,這里不再羅列。
(2)fpm_pctl_perform_idle_server_maintenance_heartbeat():
這是進程管理實現(xiàn)的主要事件智蝠,master啟動了一個定時器腾么,每隔1s觸發(fā)一次,主要用于dynamic杈湾、ondemand模式下的worker管理解虱,master會定時檢查各worker pool的worker進程數(shù),通過此定時器實現(xiàn)worker數(shù)量的控制漆撞,處理邏輯如下:
static void fpm_pctl_perform_idle_server_maintenance(struct timeval *now)
{
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
struct fpm_child_s *last_idle_child = NULL; //空閑時間最久的worker
int idle = 0; //空閑worker數(shù)
int active = 0; //忙碌worker數(shù)
for (child = wp->children; child; child = child->next) {
//根據(jù)worker進程的fpm_scoreboard_proc_s->request_stage判斷
if (fpm_request_is_idle(child)) {
//找空閑時間最久的worker
...
idle++;
}else{
active++;
}
}
...
//ondemand模式
if (wp->config->pm == PM_STYLE_ONDEMAND) {
if (!last_idle_child) continue;
fpm_request_last_activity(last_idle_child, &last);
fpm_clock_get(&now);
if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) {
//如果空閑時間最長的worker空閑時間超過了process_idle_timeout則殺掉該worker
last_idle_child->idle_kill = 1;
fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
}
continue;
}
//dynamic
if (wp->config->pm != PM_STYLE_DYNAMIC) continue;
if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
//空閑worker太多了殴泰,殺掉
last_idle_child->idle_kill = 1;
fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
wp->idle_spawn_rate = 1;
continue;
}
if (idle < wp->config->pm_min_spare_servers) {
//空閑worker太少了,如果總worker數(shù)未達到max數(shù)則fork
...
}
}
}
(3)fpm_pctl_heartbeat():
這個事件是用于限制worker處理單個請求最大耗時的浮驳,php-fpm.conf中有一個request_terminate_timeout
的配置項悍汛,如果worker處理一個請求的總時長超過了這個值那么master將會向此worker進程發(fā)送kill -TERM
信號殺掉worker進程,此配置單位為秒至会,默認值為0表示關閉此機制离咐,另外fpm打印的slow log也是在這里完成的。
static void fpm_pctl_check_request_timeout(struct timeval *now)
{
struct fpm_worker_pool_s *wp;
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
int terminate_timeout = wp->config->request_terminate_timeout;
int slowlog_timeout = wp->config->request_slowlog_timeout;
struct fpm_child_s *child;
if (terminate_timeout || slowlog_timeout) {
for (child = wp->children; child; child = child->next) {
//檢查當前當前worker處理的請求是否超時
fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);
}
}
}
}
除了上面這幾個事件外還有一個沒有提到奉件,那就是ondemand模式下master監(jiān)聽的新請求到達的事件宵蛀,因為ondemand模式下fpm啟動時是不會預創(chuàng)建worker的,有請求時才會生成子進程瓶蚂,所以請求到達時需要通知master進程糖埋,這個事件是在fpm_children_create_initial()
時注冊的,事件處理函數(shù)為fpm_pctl_on_socket_accept()
窃这,具體邏輯這里不再展開瞳别,比較容易理解征候。
到目前為止我們已經(jīng)把fpm的核心實現(xiàn)介紹完了,事實上fpm的實現(xiàn)還是比較簡單的祟敛。