本文開始會涉及寫源代碼, FPM源代碼目錄位於PHP源代碼目錄下的sapi/fpmphp
FPM大體的多進程模型就是:一個master進程,多個worker進程.
master進程負責管理調度,worker進程負責處理客戶端(nginx)的請求.
master負責建立並監聽(listen)網絡鏈接,worker負責接受(accept)網絡鏈接.
對於一個工做池,只有一個監聽socket, 多個worker共用一個監聽socket.nginx
master進程與worker進程之間,經過信號(signals)和管道(pipe)通訊.api
FPM支持多個工做池(worker pool), FPM的工做池能夠簡單的理解爲監聽多個網絡的多個FPM實例,只不過多個池都由一個master進程管理.
這裏只考慮一個工做池的狀況,理解了一個工做池,多個工做池也容易.性能優化
Unix類操做系統經過fork調用新建子進程.網絡
int pid = fork();
fork函數,能夠簡單的理解爲克隆一份進程,包含全局變量的複製.
父子進程幾乎如出一轍,是兩個獨立的進程,兩個進程使用同一份代碼.在fork以前運行的代碼也同樣.
兩個進程之因此擁有不一樣的功能.主要就是在fork以後,父進程返回的子進程pid(大於零),子進程返回的pid等於0.
重複一下,fork以後的代碼也是相同的,因爲返回的pid 不同,依據條件判斷,父子進程在fork以後所運行的代碼塊不同.dom
注:如今操做系統對fork進程複製作了性能優化,好比寫時複製(copy-on-write ),這是實現細節.說成進程克隆,是了便於理解socket
FPM 默認是以守護進程方式運行.函數
daemonize = yes
配置爲daemonize = no, 前臺運行,有助於調試oop
FPM啓動後,有些建立守護進程常見的代碼.若是隻想專一瞭解fpm,守護進程這塊代碼可跳過.
因爲FPM 默認是以守護進程方式運行,這裏作個簡單的介紹:性能
爲了和控制檯tty分離,fpm啓動進程,會建立子進程(這個子進程就是後來的master進程)
啓動進程建立一個管道pipe 用於和子進程通訊,子進程完成初始化後,會經過這個管道給啓動進程發消息,
啓動進程收到消息後,簡單處理後退出,由這個子進程負責後續工做.
平時,咱們看到的 fpm master進程,實際上是第一個子進程.
fpm 前臺運行時(daemonize = no) ,沒有這個fork的過程,啓動進程就是master進程
文件fpm_main.c 裏的main函數,是fpm服務啓動入口,依次調用函數:
main -> fpm_init -> fpm_unix_init_main ,代碼以下:
//fpm_unix.c if (fpm_global_config.daemonize) { ... if (pipe(fpm_globals.send_config_pipe) == -1) { zlog(ZLOG_SYSERROR, "failed to create pipe"); return -1; } /* then fork */ pid_t pid = fork(); ... }
worker進程建立函數爲fpm_children_make:
//fpm_children.c int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) pid_t pid; struct fpm_child_s *child; int max; static int warned = 0; //calculate max value ... while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max)) { warned = 0; child = fpm_resources_prepare(wp); if (!child) { return 2; } pid = fork(); switch (pid) { case 0 : fpm_child_resources_use(child); fpm_globals.is_child = 1; fpm_child_init(wp); return 0; case -1 : fpm_resources_discard(child); return 2; default : child->pid = pid; fpm_clock_get(&child->started); fpm_parent_resources_use(child); } } ... return 1; }
依據fpm配置
pm = static 或 ondemand 或 dynamic
有三種建立worker進程的狀況:
//fpm_children.c if (wp->config->pm == PM_STYLE_ONDEMAND) { wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s)); ... memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s)); fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp); wp->socket_event_set = 1; fpm_event_add(wp->ondemand_event, 0); return 1; }3,dynamic: 依據配置動態建立.
以上三種子進程建立方式的共同點是:都位於函數fpm_run內.
fpm_run是fpm 多進程模型的關鍵節點,
master進程會調用裏面的fpm_event_loop,無限循環,不會返回fpm_run
worker進程會在fpm_run返回後,在後續的while語句無限循環.
//fpm.c int fpm_run(int *max_requests) { struct fpm_worker_pool_s *wp; for (wp = fpm_worker_all_pools; wp; wp = wp->next) { int is_parent; is_parent = fpm_children_create_initial(wp); if (!is_parent) { goto run_child; } /* handle error */ if (is_parent == 2) { fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET); fpm_event_loop(1); } } /* run event loop forever */ fpm_event_loop(0); run_child: fpm_cleanups_run(FPM_CLEANUP_CHILD); *max_requests = fpm_globals.max_requests; return fpm_globals.listening_socket; }
master進程無限循環fpm_event_loop,主要處理定時任務和IO事件.
這裏內容較多,另文介紹.
worker進程無限循環,接受fast-cgi請求,交給PHP 解釋引擎處理
//fpm_main.c //fcgi_accept_request 函數返回值小於0 時,循環退出。 while (fcgi_accept_request(&request) >= 0) { ... //php解釋引擎處理文件 php_execute_script(&file_handle TSRMLS_CC); ... }
//fastcgi.c int fcgi_accept_request(fcgi_request *req) { while (1) { //fd>0 長連接,多個請求一個鏈接 //fd<0 短連接,一個請求一個鏈接 if (req->fd < 0) { while (1) { //in_shutdown 全局變量,優雅退出的一個開關. if (in_shutdown) { return -1; } int listen_socket = req->listen_socket; FCGI_LOCK(req->listen_socket); req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len); FCGI_UNLOCK(req->listen_socket); } }else if (in_shutdown) { return -1; } if (fcgi_read_request(req)) { return req->fd; } } }
空閒時:
對於長鏈接(少用),worker 進程會阻塞在fcgi_read_request裏的read函數,等待請求.
對於短鏈接(經常使用),worker 進程會阻塞在accept函數,等待鏈接.
以監聽端口方式爲例,函數調用過程
main
fpm_sockets_init_main
fpm_socket_af_inet_listening_socket
fpm_sockets_get_listening_socket
fpm_sockets_new_listening_socket
//fpm_sockets.c static int fpm_sockets_new_listening_socket(struct fpm_worker_pool_s *wp, struct sockaddr *sa, int socklen) { int flags = 1; int sock; mode_t saved_umask = 0; sock = socket(sa->sa_family, SOCK_STREAM, 0); if (0 > setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags))) { zlog(ZLOG_WARNING, "failed to change socket attribute"); } if (wp->listen_address_domain == FPM_AF_UNIX) { if (fpm_socket_unix_test_connect((struct sockaddr_un *)sa, socklen) == 0) { zlog(ZLOG_ERROR, "An another FPM instance seems to already listen on %s", ((struct sockaddr_un *) sa)->sun_path); close(sock); return -1; } unlink( ((struct sockaddr_un *) sa)->sun_path); saved_umask = umask(0777 ^ wp->socket_mode); } if (0 > bind(sock, sa, socklen)) { zlog(ZLOG_SYSERROR, "unable to bind listening socket for address '%s'", wp->config->listen_address); if (wp->listen_address_domain == FPM_AF_UNIX) { umask(saved_umask); } close(sock); return -1; } if (wp->listen_address_domain == FPM_AF_UNIX) { char *path = ((struct sockaddr_un *) sa)->sun_path; umask(saved_umask); if (0 > fpm_unix_set_socket_premissions(wp, path)) { close(sock); return -1; } } if (0 > listen(sock, wp->config->listen_backlog)) { zlog(ZLOG_SYSERROR, "failed to listen to address '%s'", wp->config->listen_address); close(sock); return -1; } return sock; }
對於worker進程,fpm_run返回監聽套接字(listen socket)
//fpm.c int fpm_run(int *max_requests){ ... return fpm_globals.listening_socket; //恆爲0 }
這個返回的監聽套接字,最後將傳遞給accept函數,等待鏈接.
當是,這個函數老是返回0,0號文件一般是標準輸入,哪裏不對?
原來0號文件被綁到了監聽套接字上(dup2).
//fpm_stdio.c int fpm_stdio_init_child(struct fpm_worker_pool_s *wp) { ... if (wp->listening_socket != STDIN_FILENO) { if (0 > dup2(wp->listening_socket, STDIN_FILENO)) { zlog(ZLOG_SYSERROR, "failed to init child stdio: dup2()"); return -1; } } return 0; }
因爲多個worker 共用一個監聽套接字,這裏accept先後加了加鎖和解鎖,避免驚羣效應.
//fastcgi.c int fcgi_accept_request(fcgi_request *req) { ... FCGI_LOCK(req->listen_socket); req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len); FCGI_UNLOCK(req->listen_socket); ... }
事實上,如今多數的操做unix類系統,這個加鎖和解鎖是沒必要要的,
操做系統內核已處理好了這個問題.
//fastcgi.c # ifdef USE_LOCKING # define FCGI_LOCK(fd) \ do { \ struct flock lock; \ lock.l_type = F_WRLCK; \ lock.l_start = 0; \ lock.l_whence = SEEK_SET; \ lock.l_len = 0; \ if (fcntl(fd, F_SETLKW, &lock) != -1) { \ break; \ } else if (errno != EINTR || in_shutdown) { \ return -1; \ } \ } while (1) # else # define FCGI_LOCK(fd) # endif
咱們看到,若是沒定義USE_LOCKING,FCGI_LOCK是空的,FCGI_UNLOCK相似.
而fpm 默認的編譯配置就是沒定義USE_LOCKING,因此accept 以前默認沒加鎖.
咱們看到fpm的worker進程是阻塞的.FPM配置
events.mechanism = epoll
這個IO多路複用配置worker進程沒用到.(master進程管理用到).
Nginx和Tomcat一個worker可同時處理多個鏈接
FPM一個worker可同時只能處理一個個鏈接
這是PHP FPM 和 Nginx和Tomcat 的重大區別.