1.進程管理模式
PHP-FPM由1個master進程和N個worker進程組成。其中,Worker進程由master進程fork而來。
PHP-FPM有3種worker進程管理模式。
1. Static
初始化時調用fpm_children_make(wp,0,0,1)函數fork出pm.max_children數量的worker進程,后續不再動態增減worker進程數量
2. Dynamic
初始化時調用fpm_children_make(wp,0,0,1)函數fork出pm.start_servers數量的worker進程,然后由每隔1秒觸發的心跳事件fpm_pctl_perform_idle_server_maintenance()來維護空閑woker進程數量:空閑worker進程數量若多於pm.max_spare_servers則kill進程,若少於pm.min_spare_servers則fork進程。
3. Ondemand
初始化時不生成worker進程,但注冊事件ondemand_event監聽listening_socket。當listen_socket收到request,先檢查是否存在已生成的空閑的worker進程,若存在就使用這個空閑進程,否則fork一個新的進程。每隔1秒觸發的心跳事件fpm_pctl_perform_idle_server_maintenance()會kill掉空閑時間超過pm.process_idle_timeout的worker進程
2. 標准IO
FastCGI的典型流程如下:
(1) web server(例如nginx或apache)接受到一個請求。然后,web server通過unix域socket或TCP socket連接到FastCGI應用。
(2) FastCGI應用可以選擇接受或拒絕這個連接。如果接受了連接,FastCGI應用會試圖從stream中讀取到一個packet
(3) Web server發送的第一個packet是BEGIN_REQUEST packet。BEGIN_REQUEST packet包含一個獨一無二的request ID。所有該request的后續packet都被這個ID標記。
Unix系統中,標准輸入的文件描述符是0,標准輸出的文件描述符是1,標准錯誤輸出的文件描述符是2,宏定義如下:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
PHP-FPM重定向了這三個標准IO。
在master進程中,STDIN_FILENO(0)和STDOUT_FILENO(1)均重定向到”/dev/null”。 STDERR_FILENO(2)重定向到error_log。
在worker進程中,STDIN_FILENO(0)重定向到listening_socket。如果catch_workers_output為no的話,STDOUT_FILENO(1)和STDERR_FILENO(2)均重定向到”/dev/null”。否則,STDOUT_FILENO(1)重定向到1個pipe的寫端,而這個pipe的fd讀端保存於master進程child鏈表對應的child節點結構的fd_stdout元素上。同樣的,STDERR_FILENO(2)也重定向到1個pipe的寫端,而這個pipe的讀端fd保存於master進程child鏈表對應的child節點結構的fd_stderr元素上。以上兩個位於master進程的pipe讀端由master進程的reactor進行監聽。
3. 進程間通信模型
PHP-FPM中的進程間通信主要分為
1. Master進程和worker進程之間的通信
前面講過master進程和worker進程間有兩條pipe。Worker進程向STDOUT_FILE或STDERR_FILENO中寫信息,master進程收到信息后寫入log。Master進程用reactor監聽兩個pipe
2. Web server與worker進程之間的通信
Worker進程阻塞在accept(listening socket)監聽web server
3. Web server與master進程之間的通信
當pm模式是ondemand時,master進程會在reactor注冊listening_socket的監聽事件。當有request到來,master進程將生成一個worker進程
PHP-FPM采用的進程模型是進程池。Worker進程繼承由master進程socket(),bind(),listen()的socket fd並直接阻塞在accept()上。當有一個request到來,進程池中的一個worker進程接受request。當這個worker進程完成執行,就會返回進程池等待新的request。這事實上是leader/follower模式。在leader/follower模式中,僅有leader阻塞等待,其他進程都在sleep。同樣的,在FPM中,由於linux內核解決了accept()的驚群問題,新request同樣只會喚醒一個worker進程。在這里,leader的繼任是由linux內核決定的(當然,你也可以用mutex守衛accept代碼段來確保leader只有一位)。
Worker進程處理所有IO和邏輯。Master進程負責worker進程的生成和銷毀。Master進程的Reactor注冊了三個可讀事件和四個定時器事件。當pm是ondemand時,額外注冊一個可讀事件。三個可讀事件分別是1個信號事件,2個pipe事件。
Fpm_event_s 結構:
struct fpm_event_s { int fd; struct timeval timeout; struct timeval frequency; void (*callback)(struct fpm_event_s *, short, void *); void *arg; int flags; int index; short which; };
Flags代表該事件的類型。FPM中flags的值有三種:
FPM_EV_READ : 可讀事件
FPM_EV_PERSIST : 心跳事件
FPM_EV_READ | FPM_EV_EDGE : 邊緣觸發的可讀事件
Which代表該事件位於哪個事件隊列。其值有兩種:
FPM_EV_READ : 位於可讀事件隊列
FPM_EV_TIMEOUT : 位於定時器事件隊列
事件隊列的結構是雙向鏈表
typedef struct fpm_event_queue_s { struct fpm_event_queue_s *prev; struct fpm_event_queue_s *next; struct fpm_event_s *ev; } fpm_event_queue; static struct fpm_event_queue_s *fpm_event_queue_timer = NULL; static struct fpm_event_queue_s *fpm_event_queue_fd = NULL;
fpm_event_queue_timer是定時器事件隊列,fpm_event_queue_fd是可讀事件隊列。定時器事件隊列並沒有采用最小堆,紅黑樹或事件輪等結構,因為這個隊列非常小,沒有必要使用這些復雜結構。但是如果把定時器事件隊列改為升序鏈表,對性能應該會有提升。
Fd和index僅在可讀事件中使用。fd表示被監聽的文件描述符。Index的值與使用哪個IO復用API有關。在epoll和select中,index的值等於fd的值。在poll中,index是該fd在描述符集fds[]中位置的下標。在心跳事件中,fd == -1,index == -1。
Struct timeval timeout和struct timeval frequency僅在心跳事件中使用。frequency表示每隔多少時間觸發一次心跳事件,Timeout表示下一次觸發心跳事件的時刻,通常由now與frequency相加而得。在可讀事件中,這兩個結構不設置。
Signal_fd_event事件
先來看fpm中的信號處理。
int fpm_signals_init_main() /* {{{ */ { struct sigaction act; /* create socketpair*/ if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) { zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()"); return -1; } /*將兩個socket設為NONBLOCK*/ if (0 > fd_set_blocked(sp[0], 0) || 0 > fd_set_blocked(sp[1], 0)) { zlog(ZLOG_SYSERROR, "failed to init signals: fd_set_blocked()"); return -1; } /*如果程序成功地運行完畢,則自動關閉這兩個fd*/ if (0 > fcntl(sp[0], F_SETFD, FD_CLOEXEC) || 0 > fcntl(sp[1], F_SETFD, FD_CLOEXEC)) { zlog(ZLOG_SYSERROR, "falied to init signals: fcntl(F_SETFD, FD_CLOEXEC)"); return -1; } memset(&act, 0, sizeof(act)); /* 將信號處理函數設為sig_handler*/ act.sa_handler = sig_handler; /* 將所有信號加入信號集*/ sigfillset(&act.sa_mask); /* 更改指定信號的action */ 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)) { zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()"); return -1; } return 0; }
master進程注冊了SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGCHLD,SIGQUIT,並創建了socketpair sp[]。當收到這些信號時,master進程將向sp[1]中寫一個代表該信號的字符。
[SIGTERM] = 'T',
[SIGINT] = 'I',
[SIGUSR1] = '1',
[SIGUSR2] = '2',
[SIGQUIT] = 'Q',
[SIGCHLD] = 'C'
Sp[0]就是Signal_fd_event事件監聽的fd。該事件的回調函數對不同的信號(從sp[0]讀到的代表信號的字符)做出不同的反應。
信號SIGCHLD:
調用fpm_children_bury();該函數調用waitpid()分析子進程的status,根據情況決定是否重啟子進程。
信號SIGINT , SIGTERM:
調用fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);把fpm的狀態改為terminating
信號SIGQUIT
調用fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET); 把fpm的狀態改為finishing
信號SIGUSR1
調用fpm_stdio_open_error_log(1)重啟error log file
調用fpm_log_open(1)重啟access log file 並重啟所有子進程
信號SIGUSR2
調用fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET); 把fpm的狀態改為reloading
子進程的信號處理
子進程關閉了socketpair,並把SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGHLD重新設為默認動作,把SIGQUIT設為soft quit。
int fpm_signals_init_child() /* {{{ */ { struct sigaction act, act_dfl; memset(&act, 0, sizeof(act)); memset(&act_dfl, 0, sizeof(act_dfl)); act.sa_handler = &sig_soft_quit; // 當system call或library function阻塞時一個信號到來。系統默認會返回錯誤並設置errno為EINTR.這里設為自動重啟 act.sa_flags |= SA_RESTART; act_dfl.sa_handler = SIG_DFL; close(sp[0]); close(sp[1]); if (0 > sigaction(SIGTERM, &act_dfl, 0) || 0 > sigaction(SIGINT, &act_dfl, 0) || 0 > sigaction(SIGUSR1, &act_dfl, 0) || 0 > sigaction(SIGUSR2, &act_dfl, 0) || 0 > sigaction(SIGCHLD, &act_dfl, 0) || 0 > sigaction(SIGQUIT, &act, 0)) { zlog(ZLOG_SYSERROR, "failed to init child signals: sigaction()"); return -1; } zend_signal_init(); return 0; }
其一是可讀事件隊列。其二是定時器(timer)隊列。