点击上方蓝字关注
杨通,新房研发部研发工程师,负责新房Link研发工作。2015年加入链家网(现贝壳找房),曾在大数据、新房研发部等部门工作。
我们看到处于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的最后处理时间
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.h
12 int running_children; 当前正在执行请求的Worker的个数
13 int idle_spawn_rate; Worker增加时的增加速度,Worker进程创建时用到
14 int warn_max_children;
15#if 0
16 int warn_lq;
17#endif
18 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_ACL
26 void *socket_acl;
27#endif
28};
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进程的scoreboard
50};
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_TIMES
73 struct tms cpu_accepted;
74 struct timeval cpu_duration;
75 struct tms last_request_cpu;
76 struct timeval last_request_cpu_duration;
77#endif
78 size_t memory;
79};
所以用一张图来表示的话,所有的结构关系如下:
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 process
39 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实现的事件机制来管理所有的事件。
只有当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模式就是通过该方法来管理的。
我们之前提到过,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方法里面已经被触发了。
分析完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所有进程管理有关的结构和分支。
我们之前提到,所有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#endif
12 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进程中访问这些共享内存。
在代码执行角度来看,FPM的完整周期可以分为以下五个步骤:
module_startup
request_startup
execute_script
request_shutdown
module_shutdown
其中module_start_up和module_shutdown只有在FPM服务启动和关闭时执行,其他三个步骤都是在每个请求之间完成。关于这几个方法的具体逻辑,大家可以参考:main/main.c,因为里面都是些串行逻辑,没有什么复杂步骤,我们这里就不展开介绍。
至此,FPM的主干分析流程已经梳理完了,下一章我们将分析PHP的变量存储和内存管理部分。
作者:杨 通
监审:程天亮
编辑:钟 艳
网址:tech.lianjia.com
更
多
精
彩
请猛戳右边二维码
关注我们的公众号
产品技术先行