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

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

FPM多進程輪廓:

FPM大致的多進程模型就是:一個master進程,多個worker進程.
master進程負責管理調度,worker進程負責處理客戶端(nginx)的請求.
master負責創建并監聽(listen)網絡連接,worker負責接受(accept)網絡連接.
對于一個工作池,只有一個監聽socket, 多個worker共用一個監聽socket.

master進程與worker進程之間,通過信號(signals)和管道(pipe)通信.

FPM支持多個工作池(worker pool), FPM的工作池可以簡單的理解為監聽多個網絡的多個FPM實例,只不過多個池都由一個master進程管理.
這里只考慮一個工作池的情況,理解了一個工作池,多個工作池也容易.

fork()函數

Unix類操作系統通過fork調用新建子進程.

int pid = fork();

fork函數,可以簡單的理解為克隆一份進程,包含全局變量的復制.
父子進程幾乎一模一樣,是兩個獨立的進程,兩個進程使用同一份代碼.在fork之前運行的代碼也一樣.
兩個進程之所以擁有不同的功能.主要就是在fork之后,父進程返回的子進程pid(大于零),子進程返回的pid等于0.
重復一下,fork之后的代碼也是相同的,由于返回的pid 不一樣,依據條件判斷,父子進程在fork之后所運行的代碼塊不一樣.

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

守護進程(daemonize)

FPM 默認是以守護進程方式運行.

daemonize = yes

配置為daemonize = no, 前臺運行,有助于調試

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容