PHP FPM源代碼反芻品味之三: 多進程模型

本文開始會涉及寫源代碼, FPM源代碼目錄位於PHP源代碼目錄下的sapi/fpmphp

FPM多進程輪廓:

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進程管理.
這裏只考慮一個工做池的狀況,理解了一個工做池,多個工做池也容易.性能優化

fork()函數

Unix類操做系統經過fork調用新建子進程.網絡

int pid = fork();

fork函數,能夠簡單的理解爲克隆一份進程,包含全局變量的複製.
父子進程幾乎如出一轍,是兩個獨立的進程,兩個進程使用同一份代碼.在fork以前運行的代碼也同樣.
兩個進程之因此擁有不一樣的功能.主要就是在fork以後,父進程返回的子進程pid(大於零),子進程返回的pid等於0.
重複一下,fork以後的代碼也是相同的,因爲返回的pid 不同,依據條件判斷,父子進程在fork以後所運行的代碼塊不同.dom

注:如今操做系統對fork進程複製作了性能優化,好比寫時複製(copy-on-write ),這是實現細節.說成進程克隆,是了便於理解socket

守護進程(daemonize)

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進程的建立

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進程的狀況:

  1. static: 啓動時建立:
    main -> fpm_run -> fpm_children_create_initial -> fpm_children_make
  2. ondemand: 按需建立,有請求才建立.
    啓動時,註冊建立事件.事件的細節是:監聽socket(listening_socket) 可讀時:調用建立函數 fpm_pctl_on_socket_accept
    main -> fpm_run -> fpm_children_create_initial
    //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_pctl_perform_idle_server_maintenance -> fpm_children_make
    fpm_pctl_perform_idle_server_maintenance 會定時重複運行,依據配置建立worker進程
    啓動時這個邏輯會加到timer隊列.
    後面兩個ondemand和dynamic是把建立邏輯加隊列裏,一個是IO事件,一個是timer隊列.
    有條件觸發,有鏈接或是運行時間到.
    二者都是在fpm_event_loop函數內部觸發運行.

以上三種子進程建立方式的共同點是:都位於函數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進程無限循環

master進程無限循環fpm_event_loop,主要處理定時任務和IO事件.
這裏內容較多,另文介紹.

worker進程無限循環

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函數,等待鏈接.

網絡通訊

master進程監聽套接字(listen socket)的建立

以監聽端口方式爲例,函數調用過程
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進程accept鏈接

對於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 的重大區別.

相關文章
相關標籤/搜索