PHP 内核分析 -FPM 进程管理

阅读数:304 2019 年 9 月 22 日 23:55

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.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};

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

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 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 实现的事件机制来管理所有的事件。

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#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 进程中访问这些共享内存。

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

评论

发布