以PHP7爲學習基礎,PHP7的源碼爲C編寫的。
參考書籍:《PHP內核剖析》秦鵬/著
GitHub網頁:https://github.com/pangudashu/php7-internal/blob/master/1/fpm.md
目錄
1 概述
FPM(FastCGI Process Manager)是PHP FastCGI運行模式的一個進程管理器,從它的定義可以看出,FPM的核心功能是進程管理,那麼它用來管理什麼進程呢?這個問題就需要從FastCGI說起了。
FastCGI是Web服務器(如:Nginx、Apache)和處理程序之間的一種通信協議,它是與Http類似的一種應用層通信協議,注意:它只是一種協議!具體過程如下:
(1)Web Server啓動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)
(2)FastCGI進程管理器自身初始化,啓動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的連接。
(3)當客戶端請求到達Web Server時,FastCGI進程管理器選擇並連接到一個CGI解釋器。Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。
(4)FastCGI子進程完成處理後將標準輸出和錯誤信息從同一連接返回Web Server。當FastCGI子進程關閉連接時,請求便告處理完成。FastCGI子進程接着等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。 在CGI模式中,php-cgi在此便退出了。
在上述情況中,你可以想象CGI通常有多慢。每一個Web請求PHP都必須重新解析php.ini、重新載入全部擴展並重初始化全部數據結構。使用FastCGI,所有這些都只在進程啓動時發生一次。一個額外的好處是,持續數據庫連接(Persistent database connection)可以工作。
PHP只是一個腳本解析器,你可以把它理解爲一個普通的函數,輸入是PHP腳本。輸出是執行結果,假如我們想用PHP代替shell,在命令行中執行一個文件,那麼就可以寫一個程序來嵌入PHP解析器,這就是cli模式,這種模式下PHP就是普通的一個命令工具。接着我們又想:能不能讓PHP處理http請求呢?這時就涉及到了網絡處理,PHP需要接收請求、解析協議,然後處理完成返回請求。在網絡應用場景下,PHP並沒有像Golang那樣實現http網絡庫,而是實現了FastCGI協議,然後與web服務器配合實現了http的處理,web服務器來處理http請求,然後將解析的結果再通過FastCGI協議轉發給處理程序,處理程序處理完成後將結果返回給web服務器,web服務器再返回給用戶,如下圖所示。
PHP實現了FastCGI協議的解析,但是並沒有具體實現網絡處理,一般的處理模型:多進程、多線程。
●多進程模型通常是主進程只負責管理子進程,而基本的網絡事件由各個子進程處理,nginx、fpm就是這種模式;
●多線程模型與多進程類似,只是它是線程粒度,通常會由主線程監聽、接收請求,然後交由子線程處理,memcached就是這種模式,有的也是採用多進程那種模式:主線程只負責管理子線程不處理網絡事件,各個子線程監聽、接收、處理請求,memcached使用udp協議時採用的是這種模式。
2 基本實現
fpm是一種多進程模型,它是由一個master進程和多個worker進程組成。master啓動的時候回創建一個socket,但是不會接受,處理請求,而是fork出worker子進程去接受和處理請求
fpm的實現就是創建一個master進程,在master進程中創建並監聽socket,然後fork出多個worker子進程。
worker子進程各自accept請求,子進程的處理非常簡單,它在啓動後阻塞在accept上,有請求到達後開始讀取請求數據,讀取完成後開始處理然後再返回,在這期間是不會接收其它請求的,也就是說fpm的子進程同時只能響應一個請求,只有把這個請求處理完成後纔會accept下一個請求,這一點與nginx的事件驅動有很大的區別,nginx的子進程通過epoll管理套接字,如果一個請求數據還未發送完成則會處理下一個請求,即一個進程會同時連接多個請求,它是非阻塞的模型,只處理活躍的套接字。
fpm的master進程與worker進程之間不會直接進行通信,master通過共享內存獲取worker進程的信息,比如worker進程當前狀態、已處理請求數等,當master進程要殺掉一個worker進程時則通過發送信號的方式通知worker進程。
fpm可以同時監聽多個端口,每個端口對應一個worker pool,而每個pool下對應多個worker進程,類似nginx中server概念。
在php-fpm.conf中通過[pool name]
聲明一個worker pool,每個pool各自配置監聽的地址、進程管理方式、worker進程數等。上面這個例子配置監聽端口分別爲9000、9001,pool下的worker進程監聽所屬的端口。
[web1]
listen = 127.0.0.1:9000
...
[web2]
listen = 127.0.0.1:9001
...
具體實現上worker pool通過fpm_worker_pool_s
這個結構表示,多個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; //監聽的套接字
...
//以下這個值用於master定時檢查、記錄worker數
struct fpm_child_s *children; //當前pool的worker鏈表
int running_children; //當前pool的worker運行總數
int idle_spawn_rate;
int warn_max_children;
struct fpm_scoreboard_s *scoreboard; //記錄worker的運行信息,比如空閒、忙碌worker數
...
}
2 FPM的初始化
FPM的main函數位於文件/sapi/fpm/fpm/fpm_main.c中。Fpm在啓動後首先會進行SAPI的註冊操作;接着會進入PHP聲明週期的module startup階段,在這個階段會調用各個擴展定義的MINT函數。然後進行一系列的初始化操作,最後master、worker進程進入不同的處理環節。
//sapi/fpm/fpm/fpm_main.c
int main(int argc, char *argv[])
{
...
//註冊SAPI:將全局變量sapi_module設置爲cgi_sapi_module
sapi_startup(&cgi_sapi_module);
...
//執行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內存結構並保存到全局變量中:fpm_worker_all_pools,各worker pool配置解析到fpm_worker_pool_s->config
中。
(2)fpm_scoreboard_init_main():
分配用於記錄worker進程運行信息的共享內存,按照worker pool的最大worker進程數分配,每個worker pool分配一個fpm_scoreboard_s
結構,pool下對應的每個worker進程分配一個fpm_scoreboard_proc_s
結構,各結構的對應關係如下圖。
(3)fpm_signals_init_main():
這裏會通過socketpair()
創建一個管道,這個管道並不是用於master與worker進程通信的,它只在master進程中使用,具體用途在稍後介紹event事件處理時再作說明。另外設置master的信號處理handler,當master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT這些信號時將調用sig_handler()
處理,在此函數中會把收到的信號寫入在fpm_singnals_init_main()中創建管道:
static int sp[2];
int fpm_signals_init_main()
{
struct sigaction act;
//創建一個全雙工管道
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;
}
static void sig_handler(int signo)
{
static const char sig_chars[NSIG + 1] = {
[SIGTERM] = 'T',
[SIGINT] = 'I',
[SIGUSR1] = '1',
[SIGUSR2] = '2',
[SIGQUIT] = 'Q',
[SIGCHLD] = 'C'
};
char s;
...
s = sig_chars[signo];
//將信號通知寫入管道sp[1]端
write(sp[1], &s, sizeof(s));
...
}
(4)fpm_sockets_init_main()
創建每個worker pool的socket套接字,將監聽此socket接收請求。
(5)fpm_event_init_main():
啓動master的事件管理,fpm實現了一個事件管理器用於管理IO、定時事件,其中IO事件通過kqueue、epoll、poll、select等管理,定時事件就是定時器,一定時間後觸發某個事件。
在fpm_init()
初始化完成後接下來就是最關鍵的fpm_run()
操作了,此環節將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) {
//調用fpm_children_make() fork子進程
is_parent = fpm_children_create_initial(wp);
if (!is_parent) {
goto run_child;
}
}
//master進程將進入event循環,不再往下走
fpm_event_loop(0);
run_child: //只有worker進程會到這裏
*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket; //返回監聽的套接字
}
在fork後worker進程返回了監聽的套接字繼續main()後面的處理,而master將永遠阻塞在fpm_event_loop()
,接下來分別介紹master、worker進程的後續操作。