写点什么

PHP 内核分析 -FPM 进程管理

2019 年 9 月 22 日

PHP内核分析-FPM进程管理

1. 启动过程

启动模式


从代码执行角度来讲,PHP 是一个脚本解析器,我们可以把它理解为一个普通程序,输入的是 PHP 脚本,输出的是执行结果。了解 PHP 的内核之前,我们先看图来介绍 PHP 体系的整体架构。


我们看到处于 PHP 架构内最顶层的就是 SAPI 层(SAPI 是 Server Application Programming Interface)的缩写。PHP 通过实现 SAPI 接口来和外部交互。PHP 的源码中集成了我们能用到的


各种模式,比如:apache2handler,cgi,cli,embed,fpm,litespeed,phpdbg 等。具体可以查看 sapi 目录。在我们实际工作中,最常见是的 CLI 和 FPM 模式,下面的内容我们将围绕 FPM 模式进行分析。


注 1:本博客中所有的代码都是基于 PHP7.2.5


注 2:所有的目录都是相对于 PHP 代码的根目录


启动过程


FPM(FastCGI Process Manager)是 PHP FastCGI 运行模式的一个进程管理器,它的核心功能就是管理 PHP 进程。概括来讲,FPM 的实现就是创建一个 Master 进程,在 Master 进程中创建并监听 socket,然后 fork 出多个 Worker 进程。各个 Worker 进程阻塞在 accept 方法处,有请求到达时开始读取请求数据,然后开始处理请求并返回。Worker 进程处理当前请求时不会再接收其他请求,也就是说 FPM 的 Worker 进程同时只能响应一个请求,处理完当前请求后才开始 accept 下一个请求。


详细来讲,FPM 的 Master 启动过程可以分为如下几步:(具体可参考源码 sapi/fpm/fpm/fpm_main.c:1570L)


执行 sapi_startup 方法,实际上就是将全局变量 sapi_module 设置为 cgi_sapi_module,方法细节参考:main/SAPI.c:78L


执行 cgi_sapi_module.startup 方法(sapi/fpm/fpm/fpm_main.c:1810L),追踪下去我们发现执行的是 php_module_startup 方法(sapi/fpm/fpm/fpm_main.c:1810L),方法细节参考:main/main.c:2083L


执行 fpm_init 方法,初始化当前的 FPM 配置,方法细节参考:sapi/fpm/fpm/fpm.c:46L,初始化过程有几个关键步骤:


执行 fpm_conf_init_main 方法,其实就是解析配置文件,保存到 fpm_worker_all_pools 这个结构中,我们会在下节解析这个结构;


执行 fpm_scoreboard_init_main 方法,为每个 Worker Pool 生成一个 fpm_worker_pool_s 结构,用来存储当前 Worker Pool 的运行时信息,同时为每个 Pool 里面的 Worker 进程创建一个 scoreboard 保存这个进程的运行时信息;


执行 fpm_signals_init_main 方法,主要是使用 socketpair 方法创建一个双工 Socket 通道,用于处理外部发送给 Master 进程的信号,同时初始化信号的回调方法;


执行 fpm_sockets_init_main 方法,为每个 Worker Pool 创建 Socket;


执行 fpm_event_init_main 方法,初始化事件管理机制,用于管理 IO 和定时事件;


FPM 的事件管理是基于 kqueue、epoll、poll、select 等实现的,在解析完日志文件后会调用 fpm_event_pre_init 方法确定使用哪种方式来管理。


执行 fpm_run 方法,创建 Worker 进程,创建完成后 Master 进程阻塞 fpm_event_loop,而 Worker 进程会退出 fpm_run,然后阻塞在 fcgi_accept_request 方法,等待请求的到来。


我们来看下 fpm_run 方法的具体实现:


 1/*  children: return listening socket 2    parent: never return */ 3int fpm_run(int *max_requests) /* {{{ */ 4{ 5    struct fpm_worker_pool_s *wp; 6    /* create initial children in all pools */ 7    for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 8        int is_parent; 9        is_parent = fpm_children_create_initial(wp);10        //fpm_children_create_initial方法用来真正创建Worker进程,11        //逻辑是:如果是ONDEMAND模式管理,Master并不会创建Worker,而是监听Worker Pool的端口号,当端口可读时回调fpm_pctl_on_socket_accept方法创建Worker进程12        //否则调用fpm_children_make方法创建Worker进程,返回创建结果13        //Worker进程直接goto到下面14        if (!is_parent) {15            goto run_child;16        }   17        /* handle error */18        if (is_parent == 2) {19            fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);20            fpm_event_loop(1);21        }   22    }   23    /* run event loop forever */24    fpm_event_loop(0); //创建完成后Master进程阻塞在这里25run_child: /* only workers reach this point */26    fpm_cleanups_run(FPM_CLEANUP_CHILD);27    *max_requests = fpm_globals.max_requests;28    return fpm_globals.listening_socket; //Worker进程返回main方法,继续执行后面代码29}30/* }}} */
复制代码


FPM 内部定义了 Worker 进程在处理请求的整个过程,具体可以分为以下六个阶段:


  • FPM_REQUEST_ACCEPTING :等待请求阶段

  • FPM_REQUEST_READING_HEADERS :读取 fastcgi 传递过来的请求头,比较重要

  • FPM_REQUEST_INFO :将当前请求 URI,METHOD,Query String,Auth User 等信息写入 Worker 进程的 Scoreboard

  • FPM_REQUEST_EXECUTING :请求执行

  • FPM_REQUEST_END :只有定义该状态,没有实际的方法

  • FPM_REQUEST_FINISHED :请求结束阶段,同时更新 Scoreboard 的最后处理时间


2. 进程管理

Master 进程在创建完所有的 Worker 进程后,并不会监听端口处理请求(Worker 进程管理方式是 ONDEMANDD 模式除外),而是阻塞在事件循环中。在运行期间,Master 进程会通过共享内存获取 Worker 进程的运行状态,比如当前状态,已经处理的请求数等。当 Master 要杀掉一个 Worker 进程时,会向 Worker 进程发送信号。


数据结构


FPM 能同时监听多个端口,在 FPM 内部每个端口对应一个 Worker Pool,每个 Worker Pool 中可以创建多个 Worker 进程,其关系如下:


Master 进程管理 Worker Pool 和 Worker 进程主要是通过如下的数据结构来实现的:


 1顶级结构: 2struct fpm_worker_pool_s { 3    struct fpm_worker_pool_s *next;              指向下一个Worker Pool 4    struct fpm_worker_pool_config_s *config;    指向当前Worker Pool的配置结构 5    char *user, *home;                           6    enum fpm_address_domain listen_address_domain; 7    int listening_socket; 8    int set_uid, set_gid;                       9    int socket_uid, socket_gid, socket_mode;10    /* runtime */11    struct fpm_child_s *children;                指向当前Pool的所有Child,双向链表,参考fpm_children.h12    int running_children;                        当前正在执行请求的Worker的个数13    int idle_spawn_rate;                        Worker增加时的增加速度,Worker进程创建时用到14    int warn_max_children;15#if 016    int warn_lq;17#endif18    struct fpm_scoreboard_s *scoreboard;        当前Pool的scoreboard,见下面结构19    int log_fd;20    char **limit_extensions;21    /* for ondemand PM */22    //当Worker管理模式为ONDEMEND模式时Master进程关注的事件,见fpm_children_create_initial方法23    struct fpm_event_s *ondemand_event;            24    int socket_event_set;25#ifdef HAVE_FPM_ACL26    void *socket_acl;27#endif28};29Worker Pool的scoreboard,主要记录当前Pool的Worker运行状态30struct fpm_scoreboard_s {31    union {32        atomic_t lock;     //因为有多个进程会写,所以需要有个锁保护33        char dummy[16];34    };  35    char pool[32];36    int pm; 37    time_t start_epoch;38    int idle;39    int active;40    int active_max;41    unsigned long int requests;42    unsigned int max_children_reached;43    int lq; 44    int lq_max;45    unsigned int lq_len;46    unsigned int nprocs;47    int free_proc;48    unsigned long int slow_rq;49    struct fpm_scoreboard_proc_s *procs[]; //指向所有的Worker进程的scoreboard50};51Worker进程的scoreboard,主要记录当前Worker处理请求相关的信息52struct fpm_scoreboard_proc_s {53    union {54        atomic_t lock;55        char dummy[16];56    };57    int used;58    time_t start_epoch;59    pid_t pid;60    unsigned long requests;61    enum fpm_request_stage_e request_stage;62    struct timeval accepted;63    struct timeval duration;64    time_t accepted_epoch;65    struct timeval tv;66    char request_uri[128];67    char query_string[512];68    char request_method[16];69    size_t content_length; /* used with POST only */70    char script_filename[256];71    char auth_user[32];72#ifdef HAVE_TIMES73    struct tms cpu_accepted;74    struct timeval cpu_duration;75    struct tms last_request_cpu;76    struct timeval last_request_cpu_duration;77#endif78    size_t memory;79};
复制代码


所以用一张图来表示的话,所有的结构关系如下:


3. 事件机制

FPM 的 Master 进程虽然从来不处理实际的请求,但需要要处理外部信号、定时任务等。在 ONDEMAND 模式下也需要监听 Socket 端口来创建新的 Worker 进程。所以 FPM 内部也实现了简单的事件机制来管理所有的内外部事件。具体结构如下:sapi/fpm/fpm/fpm_events.h:32L


1struct fpm_event_module_s {2    const char *name;            //Module名称:epoll、kqueue等3    int support_edge_trigger;    //是否支持边缘触发,目前只有epoll和kqueue支持4    int (*init)(int max_fd);     //监听的最大fd数量5    int (*clean)(void);            //下面是几个回调方法,每个模块都有对应的实现6    int (*wait)(struct fpm_event_queue_s *queue, unsigned long int timeout);7    int (*add)(struct fpm_event_s *ev);8    int (*remove)(struct fpm_event_s *ev);9};
复制代码


FPM 内部定义了两种事件,分别是 Timer 事件和 FD 事件,二者公用同一个结构存储。在 Master 进程开始之前,会定义两个队列,分别为存储 Timer 事件和 FD 事件,具体参考如下代码:


 1struct fpm_event_s { 2    int fd;                                                       //只有在FD类型事件才使用 3    struct timeval timeout;                                       //Timer到期时间 4    struct timeval frequency; 5    void (*callback)(struct fpm_event_s *, short, void *);       //回调方法 6    void *arg; 7    int flags; 8    int index;                 9    short which;                                                 //关注的事件10};11//事件队列12typedef struct fpm_event_queue_s {13    struct fpm_event_queue_s *prev;14    struct fpm_event_queue_s *next;15    struct fpm_event_s *ev;16} fpm_event_queue;17//初始化两个事件队列18static struct fpm_event_queue_s *fpm_event_queue_timer = NULL;19static struct fpm_event_queue_s *fpm_event_queue_fd = NULL;
复制代码


我们打开 sapi/fpm/fpm/events 目录,我们能看到里面实现了对各种事件管理机制的封装,以 epoll.c 为例:


 1static int fpm_event_epoll_init(int max); 2static int fpm_event_epoll_clean(); 3static int fpm_event_epoll_wait(struct fpm_event_queue_s *queue, unsigned long int timeout); 4static int fpm_event_epoll_add(struct fpm_event_s *ev); 5static int fpm_event_epoll_remove(struct fpm_event_s *ev); 6//绑定回调方法 7static struct fpm_event_module_s epoll_module = {  8    .name = "epoll", 9    .support_edge_trigger = 1,10    .init = fpm_event_epoll_init,11    .clean = fpm_event_epoll_clean,12    .wait = fpm_event_epoll_wait,13    .add = fpm_event_epoll_add,14    .remove = fpm_event_epoll_remove,15};
复制代码


我们之前提过,在 FPM 启动阶段读取完配置后,会调用 fpm_event_pre_init 方法初始化 Master 进程使用的实际的事件机制。具体参考:sapi/fpm/fpm/fpm_events.c:237L


外部信号


Master 在 fpm_signals_init_main 阶段会使用 socketpair(参考:socketpair)方法创建一个通道,并且设置了对外部信号的处理方法,参考下面代码:


 1int fpm_signals_init_main() /* {{{ */ 2{ 3    struct sigaction act; 4    //创建socket通道 5    if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) { 6        zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()"); 7        return -1;  8    } 9    memset(&act, 0, sizeof(act));10    act.sa_handler = sig_handler;11    sigfillset(&act.sa_mask);12    //设置handler的处理方法13    if (0 > sigaction(SIGTERM,  &act, 0) ||14        0 > sigaction(SIGINT,   &act, 0) ||15        0 > sigaction(SIGUSR1,  &act, 0) ||16        0 > sigaction(SIGUSR2,  &act, 0) ||17        0 > sigaction(SIGCHLD,  &act, 0) ||18        0 > sigaction(SIGQUIT,  &act, 0)) {19        zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");20        return -1; 21    }   22    return 0;23}24具体的处理方法逻辑如下:25static void sig_handler(int signo) /* {{{ */26{27    static const char sig_chars[NSIG + 1] = {28        [SIGTERM] = 'T',29        [SIGINT]  = 'I',30        [SIGUSR1] = '1',31        [SIGUSR2] = '2',32        [SIGQUIT] = 'Q',33        [SIGCHLD] = 'C'34    };35    char s;36    int saved_errno;37    if (fpm_globals.parent_pid != getpid()) {38        /* prevent a signal race condition when child process39            have not set up it's own signal handler yet */40        return;41    }42    saved_errno = errno;43    s = sig_chars[signo];44    zend_quiet_write(sp[1], &s, sizeof(s)); //收到信号,并将信号转化为T/I等指令写入之前创建的通道。write(sp[1])45    errno = saved_errno;46}
复制代码


在 Master 进程调用 fpm_event_loop 进入事件循环时,会将上面创建的 sp[0]fd 上面的可读事件加入到事件监听模块,参考:sapi/fpm/fpm/fpm_events.c:355L


1    fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);2    fpm_event_add(&signal_fd_event, 0); 
复制代码


本来进程是可以处理外部信号的,但是为什么非要搞这么一个通道来将信号转化为 socket 事件?我猜是为了在事件处理这一侧打平,也就是说统一用 FPM 实现的事件机制来管理所有的事件。


Socket 事件


只有当 FPM 管理 Worker 进程使用 ONDEMAND 模式时 Master 进程才会监听外部来的请求,具体看代码:


 1int fpm_children_create_initial(struct fpm_worker_pool_s *wp) 2{ 3    if (wp->config->pm == PM_STYLE_ONDEMAND) { //如果是ONDEMAND模式 4        //初始化事件 5        wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s)); 6        if (!wp->ondemand_event) { 7            zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name); 8            // FIXME handle crash 9            return 1;10        }   11        memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));12        //将时间的回调方法设置为fpm_pctl_on_socket_accept,并且加入到事件管理机制里面13        //fpm_pctl_on_socket_accept方法比较简单,无非是创建Worker进程,设置scoreboard,然后去处理请求14        fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp);15        wp->socket_event_set = 1;16        fpm_event_add(wp->ondemand_event, 0); 17        return 1;18    }   19    //否则直接创建Worker进程20    return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1); 21}
复制代码


定时任务


FPM 运行期间会不断地执行定时任务,具体来讲分为两种:


当管理 Worker 进程方式是 DYNAMIC 时定期会 fork 或者是 kill Worker 进程,对应:fpm_pctl_perform_idle_server_maintenance_heartbeat 方法


当 Worker 进程处理请求超时时会向该 Worker 进程发送 TERM 信号,对应:fpm_pctl_heartbeat 方法


这两个方法的处理逻辑基本相似,处理过程基本上是第一次调用时会添加一个 Timer 事件,后续每当事件触发时会调用处理方法。我们看下 fpm_pctl_heartbeat 方法的源码:


 1void fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */ 2{ 3    static struct fpm_event_s heartbeat; 4    struct timeval now; 5    if (fpm_globals.parent_pid != getpid()) { 6        return; /* sanity check */ 7    }    8    //执行检查请求处理是否超时的办法,注意第一次which参数传入的是0,不会执行这段 9    if (which == FPM_EV_TIMEOUT) {10        fpm_clock_get(&now);11        fpm_pctl_check_request_timeout(&now);12        return;13    }   14    //确定心跳频率15    /* ensure heartbeat is not lower than FPM_PCTL_MIN_HEARTBEAT */16    fpm_globals.heartbeat = MAX(fpm_globals.heartbeat, FPM_PCTL_MIN_HEARTBEAT);17    /* first call without setting to initialize the timer */18    //添加一个Timer事件,回调方法仍然是自身,注意只有第一次才会添加,因为后续传入的which是FPM_EV_TIMEOUT,在上面就return了19    zlog(ZLOG_DEBUG, "heartbeat have been set up with a timeout of %dms", fpm_globals.heartbeat);20    fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_heartbeat, NULL);21    fpm_event_add(&heartbeat, fpm_globals.heartbeat);22}
复制代码


我们来追踪下 fpm_pctl_check_request_timeout 方法的执行过程:


 1static void fpm_pctl_check_request_timeout(struct timeval *now) /* {{{ */ 2{ 3    struct fpm_worker_pool_s *wp; 4    //轮询所有的worker_pool,找出request_terminate_timeout和request_slowlog_timeout,然后对每个Child执行fpm_request_check_timed_out方法 5    for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 6        int terminate_timeout = wp->config->request_terminate_timeout; 7        int slowlog_timeout = wp->config->request_slowlog_timeout; 8        struct fpm_child_s *child; 9        if (terminate_timeout || slowlog_timeout) {10            for (child = wp->children; child; child = child->next) {11                fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);12            }13        }14    }15}16void fpm_request_check_timed_out(struct fpm_child_s *child, struct timeval *now, int terminate_timeout, int slowlog_timeout) /* {{{ */17{18        //核心就在这里,如果超时,则通过fpm_pctl_kill方法向Worker进程发送TERM信号19        if (terminate_timeout && tv.tv_sec >= terminate_timeout) {20            str_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename));21            fpm_pctl_kill(child->pid, FPM_PCTL_TERM);22            zlog(ZLOG_WARNING, "[pool %s] child %d, script '%s' (request: \"%s %s%s%s\") execution timed out (%d.%06d sec), terminating",23                child->wp->config->name, (int) child->pid, purified_script_filename, proc.request_method, proc.request_uri,24                (proc.query_string[0] ? "?" : ""), proc.query_string,25                (int) tv.tv_sec, (int) tv.tv_usec);26        }27}
复制代码


fpm_pctl_perform_idle_server_maintenance_heartbeat 方法的执行流程和 fpm_pctl_heartbeat 比较相似,只不过处理逻辑比较复杂,具体逻辑大家可以自己查看。PHP 管理 Worker 进程的 DYNAMIC 模式就是通过该方法来管理的。


EventLoop


我们之前提到过,Master 进程创建完所有的 Worker 进程后就进入到了 fpm_event_loop 方法,然后一直阻塞在这个方法里面,下面我们来分析下这个方法的逻辑。


 1void fpm_event_loop(int err) /* {{{ */ 2{ 3    static struct fpm_event_s signal_fd_event; 4    //fpm_signals_get_fd这个方法返回的就是之前创建的Socketpaire的0号Socket,回调方法是fpm_got_signal 5    fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL); 6    fpm_event_add(&signal_fd_event, 0); 7    //添加做心跳检查的Timer事件 8    if (fpm_globals.heartbeat > 0) { 9        fpm_pctl_heartbeat(NULL, 0, NULL);10    }11    //添加做Worker管理的Timer事件12    if (!err) {13        fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);14    }15   //进入主循环16   while (1) {17        struct fpm_event_queue_s *q, *q2;18        struct timeval ms;19        struct timeval tmp;20        struct timeval now;21        unsigned long int timeout;22        int ret;23        //查找最近要过期的Timer事件,然后设置epoll或select的超时时间为该时间与当前的时间差。24        //也就是最多等待timeout时间就退出等待,然后执行Timer事件的回调方法,(如果提前退出等待会进行Timer的校验,如果已经到达Timer事件要过期的时间,然后才会执行回调)。这种方式在主流的服务里面比较流行,redis和nginx也是这么实现的。25        q = fpm_event_queue_timer;26        while (q) {27            if (!timerisset(&ms)) {28                ms = q->ev->timeout;29            } else {30                if (timercmp(&q->ev->timeout, &ms, <)) {31                    ms = q->ev->timeout;32                }33            }34            q = q->next;35        }36        //阻塞在这里37        ret = module->wait(fpm_event_queue_fd, timeout);38        //等待超时或者是有socket事件到达,开始处理Timer事件队列39        q = fpm_event_queue_timer;40        while (q) {41            fpm_clock_get(&now);42            if (q->ev) {43                //如果到达过期时间44                if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) {45                    //触发回调46                    fpm_event_fire(q->ev);47                    /* sanity check */48                    if (fpm_globals.parent_pid != getpid()) {49                        return;50                    }51                    //如果是FPM_EV_PERSIST类型的事件,那么更新下一次过期时间,否则删除该事件52                    if (q->ev->flags & FPM_EV_PERSIST) {53                        fpm_event_set_timeout(q->ev, now);54                    } else { /* delete the event */55                        q2 = q;56                        if (q->prev) {57                            q->prev->next = q->next;58                        }59                        if (q->next) {60                            q->next->prev = q->prev;61                        }62                        if (q == fpm_event_queue_timer) {63                            fpm_event_queue_timer = q->next;64                            if (fpm_event_queue_timer) {65                                fpm_event_queue_timer->prev = NULL;66                            }67                        }68                        q = q->next;69                        free(q2);70                        continue;71                    }72                }73            }74            q = q->next;75        }76    }77}
复制代码


分析完 fpm_event_loop 的执行过程,我们会有个疑问,我们只看到了 Timer 事件回调方法的触发,Socket 事件的回调是在什么时候触发的呢?我们再看下 wait 方法,以 epoll 为例:


 1static int fpm_event_epoll_wait(struct fpm_event_queue_s *queue, unsigned long int timeout) /* {{{ */ 2{ 3    int ret, i; 4    //进入epoll_wait 5    ret = epoll_wait(epollfd, epollfds, nepollfds, timeout); 6    //触发所有回调 7    for (i = 0; i < ret; i++) { 8        /* do we have a valid ev ptr ? */ 9        if (!epollfds[i].data.ptr) {10            continue;11        }   12        /* fire the event */13        fpm_event_fire((struct fpm_event_s *)epollfds[i].data.ptr);14        /* sanity check */15        if (fpm_globals.parent_pid != getpid()) {16            return -2;17        }18    }19    return ret;20}
复制代码


我们看到 Socket 事件的回调在 wait 方法里面已经被触发了。


Worker 行为


分析完 Master 进行的行为,我们可以简单看下 Worker 进程的行为。我们之前说过,Worker 进程就是阻塞在 accept 方法,接收 cgi 请求,然后做处理。但是与此同时,Worker 进程需要响应 Master 发送的信号,比如所 Master 通知子进程要退出等。我们来分析下 fpm_children_make 方法:


 1int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */ 2{ 3    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)) { 4        warned = 0; 5        child = fpm_resources_prepare(wp); 6        //创建Worker进程,从此Master和Worker就分道扬镳 7        pid = fork(); 8        switch (pid) { 9            //Worker进程行为10            case 0 :11                fpm_child_resources_use(child);12                fpm_globals.is_child = 1;13                fpm_child_init(wp);14                return 0;15            //Master进程行为16            default :17                child->pid = pid;18                fpm_clock_get(&child->started);19                fpm_parent_resources_use(child);20        }21    }22}
复制代码


我们再继续跟踪下 fpm_child_init 方法的行为,参考源码:sapi/fpm/fpm/fpm_children.c:146L


 1static void fpm_child_init(struct fpm_worker_pool_s *wp) /* {{{ */ 2{                3    fpm_globals.max_requests = wp->config->pm_max_requests; 4    if (0 > fpm_stdio_init_child(wp)  || 5        0 > fpm_log_init_child(wp)    || 6        0 > fpm_status_init_child(wp) || 7        0 > fpm_unix_init_child(wp)   || 8        重点关注该方法: 9        0 > fpm_signals_init_child()  ||10        0 > fpm_env_init_child(wp)    ||11        0 > fpm_php_init_child(wp)) {12        zlog(ZLOG_ERROR, "[pool %s] child failed to initialize", wp->config->name);13        exit(FPM_EXIT_SOFTWARE);14    }           15}16int fpm_signals_init_child() /* {{{ */17{18    struct sigaction act, act_dfl;19    memset(&act, 0, sizeof(act));20    memset(&act_dfl, 0, sizeof(act_dfl));21    act.sa_handler = &sig_soft_quit;22    act.sa_flags |= SA_RESTART;23    act_dfl.sa_handler = SIG_DFL;24    //关闭Master进程创建的,因为Worker进程不需要这个通道25    close(sp[0]);26    close(sp[1]);27    //注意这里的act_dfl并不是不处理,而是使用系统默认的处理方法,可以手动执行kill -15(TERM) pid执行,看下效果28    if (0 > sigaction(SIGTERM,  &act_dfl,  0) ||29        0 > sigaction(SIGINT,   &act_dfl,  0) ||30        0 > sigaction(SIGUSR1,  &act_dfl,  0) ||31        0 > sigaction(SIGUSR2,  &act_dfl,  0) ||32        0 > sigaction(SIGCHLD,  &act_dfl,  0) ||33        //我们看Worker进程只对SIGQUIT做了特殊处理,追踪下去我们看到其实是进行了些soft quit相关动作,做退出前处理34        0 > sigaction(SIGQUIT,  &act,      0)) {35        zlog(ZLOG_SYSERROR, "failed to init child signals: sigaction()");36        return -1; 37    }   38    zend_signal_init();39    return 0;40}
复制代码


另外我们还没有涉及到的一个分支是当进程管理是 ONDEMEND 模式时,Master 进程会监听对应 Worker Pool 的端口号,如果发现有连接到来,那么回调 fpm_pctl_on_socket_accept 方法。其逻辑就是创 Worker 进程,Worker 进程创建完成后会跳出 Master 进程当前的 fpm_event_loop 方法,转而去监听端口,读数据,处理请求,Master 方法继续进入 fpm_event_loop。具体可以看下 fpm_pctl_on_socket_accept 方法的实现:sapi/fpm/fpm/fpm_process_ctl.c:496L,逻辑比较简单,我们就不在这里展开。


至此,我们已经梳理完了 FPM 所有进程管理有关的结构和分支。


4. 共享内存

我们之前提到,所有 Worker Pool 和 Worker 进程的状态是保存在唯一的变量 fpm_worker_all_pools 中的,所以这就涉及到一个问题,Master 和 Worker 进程都会访问这块变量。那么 Master 进程和 Worker 进程是怎么进行共享内存的呢,我们来看下源码:(sapi/fpm/fpm/fpm_scoreboard.c:25L)


 1int fpm_scoreboard_init_main() /* {{{ */ 2{ 3    struct fpm_worker_pool_s *wp; 4    unsigned int i; 5    //外部执行worker pool数量次 6    for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 7        size_t scoreboard_size, scoreboard_nprocs_size; 8        void *shm_mem; 9        if (wp->config->pm_max_children < 1) {10            zlog(ZLOG_ERROR, "[pool %s] Unable to create scoreboard SHM because max_client is not set", wp->config->name);11            return -1; 12        }   13        if (wp->scoreboard) {14            zlog(ZLOG_ERROR, "[pool %s] Unable to create scoreboard SHM because it already exists", wp->config->name);15            return -1; 16        }   17        //计算该Pool的scoreboard需要的空间18        //fpm_scoreboard_s大小+N个fpm_scoreboard_proc_s指针大小,DYNAMIC或者是ONDEMEND模式按照最大Worker数分配19        scoreboard_size        = sizeof(struct fpm_scoreboard_s) + (wp->config->pm_max_children) * sizeof(struct fpm_scoreboard_proc_s *); 20        //N个fpm_scoreboard_proc_s结构的大小21        scoreboard_nprocs_size = sizeof(struct fpm_scoreboard_proc_s) * wp->config->pm_max_children;22        //真正分配23        shm_mem                = fpm_shm_alloc(scoreboard_size + scoreboard_nprocs_size);24    }25    return 0;26}
复制代码


看来我们需要追踪 fpm_shm_alloc 方法来看具体什么实现的共享内存,具体查看:sapi/fpm/fpm/fpm_shm.c:20L


 1void *fpm_shm_alloc(size_t size) /* {{{ */ 2{ 3    void *mem; 4    //实际调用系统调用mmap,开辟一个匿名的共享区域, 5    mem = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);  6#ifdef MAP_FAILED 7    if (mem == MAP_FAILED) { 8        zlog(ZLOG_SYSERROR, "unable to allocate %zu bytes in shared memory: %s", size, strerror(errno)); 9        return NULL;10    }   11#endif12    if (!mem) {13        zlog(ZLOG_SYSERROR, "unable to allocate %zu bytes in shared memory", size);14        return NULL;15    }   16    fpm_shm_size += size;17    return mem;18}
复制代码


mmap 是一个系统调用,一般用来申请大块内存,关于 mmap 的使用,参考:mmap。


Master 进程在 fpm_scoreboard_init_main 方法中初始化了这些共享内存,并将指针放在了全局变量 fpm_worker_all_pools 中。这样当 fork 发生后,Worker 进程仍然拥有该指针,所以也能在 Worker 进程中访问这些共享内存。


5. 生命周期

在代码执行角度来看,FPM 的完整周期可以分为以下五个步骤:


  • module_startup

  • request_startup

  • execute_script

  • request_shutdown

  • module_shutdown


其中 module_start_up 和 module_shutdown 只有在 FPM 服务启动和关闭时执行,其他三个步骤都是在每个请求之间完成。关于这几个方法的具体逻辑,大家可以参考:main/main.c,因为里面都是些串行逻辑,没有什么复杂步骤,我们这里就不展开介绍。


至此,FPM 的主干分析流程已经梳理完了,下一章我们将分析 PHP 的变量存储和内存管理部分。


作者介绍:


杨通,新房研发部研发工程师,负责新房 Link 研发工作。2015 年加入链家网(现贝壳找房),曾在大数据、新房研发部等部门工作。


本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。


原文链接:


https://mp.weixin.qq.com/s/DFF8YFknZzrOA1ulHdTcog


2019 年 9 月 22 日 23:55741

评论

发布
暂无评论
发现更多内容

「架构师训练营」第 4 周作业

小黄鱼

极客大学架构师训练营

Java批量导入去除重复数据并返回结果,我差点就被放倒了

小Q

Java 学习 程序员 架构

与第三方系统打通的N种进阶方式

棒锤🐮

架构

奈学教育荣获“中关村高新技术企业”认证

奈学教育

奈学教育

浅谈程序员的“内卷化”

数据社

第八周作业

Geek_4c1353

极客大学架构师训练营

年末十家手机银行数字化升级大盘点:谁家开发更全面?谁家建设更到位?

CECBC区块链专委会

疫情 银行 手机银行

搞微服务用阿里开源的 Nacos 真香啊!

云流

阿里巴巴 编程 开源项目

CloudQuery v1.2.1 版本发布

CloudQuery社区

数据库 开发者 运维 工具 开发工具

对比一下,你的简历是不是也写成了这样,能拿高薪才怪了

小Q

Java 学习 架构 面试 简历

当Nginx遇上Tomcat集群,又是一场负载均衡的爱恨情仇

小Q

nginx tomcat 学习 架构 面试

奈学教育荣获“中关村高新技术企业”认证

古月木易

教育 IT

科技助力餐饮,普渡送餐机器人在餐博会上被众人围观!

DT极客

腾讯云直播全解析,双11怎么买才不亏?

腾讯云视频云

腾讯云 阿里云 云直播 直播 视频

高交会现场:众多区块链项目亮相,“家谱链”惊艳全场

WX13823153201

架构训练营-week8-数据结构与算法,网络,IO

于成龙

极客大学架构师训练营 架构训练营

终于,阿里P9耐不住寂寞,以多年经验总结了地表最强SQL宝典

周老师

Java 编程 程序员 架构 面试

践行新基建,共建城市智能体,为数字经济发展提供新动能

CECBC区块链专委会

云计算 大数据

11.11 程序员的 1111 种死法

京东科技开发者

程序员 程序人生

阿里首发MySQL“完美日记”,基础+优化+事务+集群+锁+主从复制+安全备份

Java架构追梦

Java MySQL 数据库 架构 面试

重拳出击!平台经济反垄断,互联网巨头市值蒸发千亿

CECBC区块链专委会

小额贷款 反垄断

第七周作业

Geek_4c1353

极客大学架构师训练营

iptables 端口转发

田振宇

SpringBoot启动原理

云流

编程门槛 框架设计 spring Boot Starter】

实时音视频面视必备:快速掌握11个视频技术相关的基础概念

JackJiang

即时通讯 视频 实时音视频

我终于拥有自己的独立博客了。

彭宏豪95

GitHub 写作 博客 IT

从应用开发角度认识K8S

LorraineLiu

云原生 容器技术 k8s入门

当Tomcat遇上Netty,我这一系列神操作,同事看了拍手叫绝

小Q

Java 学习 程序员 架构 面试

【涂鸦物联网足迹】涂鸦云平台接口列表—万能红外遥控器

IoT云工坊

人工智能 云计算 物联网 API 红外遥控器

《我想进大厂》之Java基础夺命连环16问

艾小仙

Java 面试 编程语言 面试技巧

面试官问我redis数据类型,我回答了8种

云流

数据库 学习 java面试

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

PHP内核分析-FPM进程管理-InfoQ