美文网首页
PHP FPM源代码反刍品味之三: 多进程模型

PHP FPM源代码反刍品味之三: 多进程模型

作者: 黄洪清 | 来源:发表于2016-08-02 19:17 被阅读2197次

    本文开始会涉及写源代码, 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 的重大区别.

    相关文章

      网友评论

          本文标题:PHP FPM源代码反刍品味之三: 多进程模型

          本文链接:https://www.haomeiwen.com/subject/pvgfsttx.html