FPM模式的生命周期

FPM(FastCGI Process Manager)是一个 FastCGI 进程管理器,对于 PHP 5.3.3 之前的 PHP 来说,它只是一个补丁包。从 PHP 5.3.3 开始,PHP 集成了 PHP-FPM。PHP-FPM 提供了更好的 PHP 进程管理方式,可以有效控制内存和进程,支持平滑重启 PHP 及重载 PHP 配置。
与 CLI 模式类似,FPM 模式的生命周期也有 5 个阶段,但是又与 CLI 模式的生命周期不同,因为 FPM 是常驻内存的进程,所以其模块初始化只做一次,便进入循环,而模块关闭在进程退出时也只做一次,如图7-11所示。
-
调用 php_module_startup,加载所有模块。
-
进入循环,调用 fcgi_accept_request 实际调用的是 accept,阻塞等待请求;如果有请求进来,会被唤起,进入 php_request_startup,初始化请求。为了防止多个进程对 accept 进行抢占,出现 “惊群” 情况,增加了锁机制:
FCGI_LOCK(req->listen_socket); req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len); FCGI_UNLOCK(req->listen_socket);
但是细心的读者可以发现,FCGI_LOCK/FCGI_UNLOCK 在 Linux 下已经没有实现了:
# define FCGI_LOCK(fd) # define FCGI_UNLOCK(fd)
这是因为在 Linux 2.6 内核上,阻塞版本的 accept 系统调用已经不存在 “惊群” 了。
-
进入 php_execute_script,对脚本执行编译。
-
调用 php_request_shutdown 关闭请求,继续进入循环。
-
如果进程退出,调用 php_module_shutdown 关闭所有模块。
-
如果请求次数大于 max_requests,则跳转 5。
在 Linux 2.6 内核上,阻塞版本的 accept 系统调用已经不存在 “惊群” 了。大家可以写一个简单的程序测试下,并在父进程中绑定、监听,然后 fork 出子进程,所有的子进程都会尝试接受(accept)这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在 accept 调用上,没有被唤醒。 |
了解了 FPM 的生命周期,下面具体分析一下整个过程。
多进程管理
PHP-FPM 是多进程的服务,其中有一个 master 进程(做管理工作)和多个 worker 进程(处理数据请求)。下面我们从多进程管理角度对 PHP-FPM 展开阐述,首先讨论 master 进程和 worker 进程是如何创建的,然后讨论进程之间是如何通信的,比如 worker 进程意外退出,master 进程是如何感知并重新创建新的 worker 进程的。
进程创建
我们以 Nginx+PHP-FPM 方式为例,讲一下整个 Web 请求的过程。一般情况下,Nginx 会根据服务器的 CPU 内核数设置 worker 的进程数,而 PHP-FPM 的进程有三种设置方式:static、dynamic 和 ondemand,可以在 php-fpm.conf 里面设置:
pm = static //其他:dynamic或者ondemand
- static 模式
-
static 模式始终会保持一个固定数量的子进程,这个数量由 pm.max_children 定义,比如线上,我们可以将其设置为 512 个 worker,我们可以观察 PHP-FPM 的进程空闲数,如图7-12所示。
Figure 2. 图7-12 PHP-FPM空闲数从图7-12可以看出,随着请求量的变化,PHP-FPM 的空闲数也发生了变化。
- dynamic 模式
-
子进程的数量是动态变化的。启动时,会生成固定数量的子进程,可以理解成最小子进程数,通过 pm.start_servers 控制,而最大子进程数则由 pm.max_children 控制,子进程数会在 pm.start_servers~pm.max_children 范围内变化,另外,闲置的子进程数还可以由 pm.min_spare_servers 和 pm.max_spare_servers 两个配置参数控制。换句话说,闲置的子进程也可以有最小数目和最大数目,而如果闲置的子进程超出了 pm.max_spare_servers,则会被杀掉。
- ondemand 模式
-
这种模式和 dynamic 模式正好相反,把内存放在第一位,每个闲置进程在持续闲置了 pm.process_idle_timeout 秒后就会被杀掉。有了这个模式,到了服务器低峰期,内存自然会降下来,如果服务器长时间没有请求,就只会有一个 PHP-FPM 主进程,当然其弊端是,遇到高峰期或者 pm.process_idle_timeout 的值太小的话,无法避免服务器频繁创建进程的问题。
3 种模式对应的定义如下:
enum {
PM_STYLE_STATIC = 1,
PM_STYLE_DYNAMIC = 2,
PM_STYLE_ONDEMAND = 3
};
我们了解了 PHP-FPM 的 3 种运行模式,接下来继续介绍整个 webserver 的运行过程,如图7-13所示。

从图 7-13 可以看到,Client 通过 HTTP 方式请求 Nginx,请求由 Nginx 的 worker 进行处理,转成对应的 FastCGI,请求 FPM, accept 由 FPM 的 worker 进程处理,执行完毕再返回给 Nginx, Nginx 再进一步返回给 Client。
下面我们详细讨论一下 PHP-FPM 进程是怎么启动的。我们使用 gdb 来启动 PHP-FPM,其中 PHP-FPM 在 sbin 目录下:
gdb sbin/php-fpm
(gdb) b main
(gdb) r -y etc/php-fpm.conf
在 main 函数入口处增加断点,然后使用 r -y etc/php-fpm.conf 指定加载的配置文件,此时启动的进程并不是 master 进程,而是 calling process 进程,calling process 进程会 fork 出 master 进程,并退出。为了能够跟随 master 进程,我们使用 gdb 里面的命令,以对子进程进行跟踪:
(gdb) set follow-fork-mode child
输入 c 命令(continue):
(gdb) c
Continuing.
Breakpoint 2, 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
(gdb) bt
#0 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
#1 0x0000000000aa234b in fpm_unix_init_main () at /root/php7/book/php-7.1.0/sapi/
fpm/fpm/fpm_unix.c:495
#2 0x0000000000a8c04f in fpm_init (argc=3, argv=0x7fffffffe178, config=0x7fffffffe479
"etc/php-fpm.conf", prefix=0x0, pid=0x0, test_conf=0, run_as_root=0, force_
daemon=-1,
force_stderr=0) at /root/php7/book/php-7.1.0/sapi/fpm/fpm/fpm.c:61
#3 0x0000000000a997a5 in main (argc=3, argv=0x7fffffffe178) at /root/php7/book/
php-7.1.0/sapi/fpm/fpm/fpm_main.c:1
(gdb) n
Single stepping until exit from function fork,
which has no line number information.
[New process 16299]
[Thread debugging using libthread_db enabled]
[Switching to Thread 0x7ffff7fe0700 (LWP 16299)]
0x0000003dcc005810 in __nptl_set_robust () from /lib64/libpthread.so.0
我们可以看到,calling process 会在 fpm-init 函数中将 master 进程 fork 出来,同时自己退出。对应的代码如下:
pid_t pid = fork();
switch (pid) {
case -1 : /* error */
zlog(ZLOG_SYSERROR, "failed to daemonize");
return -1;
case 0 : /* children */
close(fpm_globals.send_config_pipe[0]);
/* close the read side of the pipe */
break;
default : /* parent */
close(fpm_globals.send_config_pipe[1]);
/* close the write side of the pipe */
……
exit(FPM_EXIT_SOFTWARE);
我们知道,对于父进程(calling process), fork 返回的 pid 是 master 进程的 pid,会走到 default 逻辑中,最终会退出进程;而 master 进程会在 fpm_run 函数中 fork 子进程(worker 进程), gdb 信息如下:
Breakpoint 2, 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
(gdb) bt
#0 0x0000003dcbcacd14 in fork () from /lib64/libc.so.6
#1 0x0000000000a8cfc1 in fpm_children_make (wp=0x1315430, in_event_loop=0, nb_to_
spawn=0, is_debug=1) at /root/php7/book/php-7.1.0/sapi/fpm/fpm/fpm_children.c:400
#2 0x0000000000a8d259 in fpm_children_create_initial (wp=0x1315430) at /root/php7/
book/php-7.1.0/sapi/fpm/fpm/fpm_children.c:453
#3 0x0000000000a8c1a2 in fpm_run (max_requests=0x7fffffffdf3c) at /root/php7/book/
php-7.1.0/sapi/fpm/fpm/fpm.c:101
#4 0x0000000000a998c8 in main (argc=3, argv=0x7fffffffe178) at /root/php7/book/
php-7.1.0/sapi/fpm/fpm/fpm_main.c:1
在函数 fpm_children_make 中,我们可以看到 static、dynamic、ondemand 这 3 种模式的不同之处,代码如下:
if (wp->config->pm == PM_STYLE_DYNAMIC) {
/* dynamic模式下,先启动pm_start_servers数量的worker进程,根据请求动态变化 */
if (! in_event_loop) { /* starting */
max = wp->config->pm_start_servers;
} else {
max = wp->running_children + nb_to_spawn;
}
} else if (wp->config->pm == PM_STYLE_ONDEMAND) {
/* ondemand模式下,启动时并不创建worker进程,按需启动 */
if (! in_event_loop) { /* starting */
max = 0; /* do not create any child at startup */
} else {
max = wp->running_children + nb_to_spawn;
}
} else { /* PM_STYLE_STATIC */
/* static模式下,启动固定数量的worker进程 */
max = wp->config->pm_max_children;
}
从代码中可以看出,在 static 模式下,会走到最后一个 else,进程数为 pm_max_children;在 dynamic 模式下,启动时,进程数为 pm_start_servers,而在 ondemand 模式下,启动时,进程数为 0。
接下来,master 会根据需要启动的子进程数进行 fork,代码如下:
/*
* fork children while:
* - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not
restart, stop or reload)
* - wp->running_children < max : there is less than the max process for
the current pool
* - (fpm_global_config.process_max < 1 || fpm_globals.running_children <
fpm_global_config.process_max):
* if fpm_global_config.process_max is set, FPM has not fork this number of
processes (globaly)
*/
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();
到这里我们明白了,php-fpm 启动时,首先启动一个 calling process,然后由 calling process 创建 master 进程,master 进程根据需要创建的子进程数创建 work 进程,其中 master 进程的 title 为 php-fpm: master process,而 worker 进程的名称为 php-fpm:pool name,其中 name 在 php-fpm.conf 里面设置:
; pool name ('www' here)
[www]
子进程修改名称的代码在函数 fpm_env_init_child 中:
char *title;
spprintf(&title, 0, "pool %s", wp->config->name);
fpm_env_setproctitle(title);
efree(title);
整个 php-fpm 进程的创建过程如图7-14所示。

讨论完进程创建的过程,下面分析一下进程是如何管理的。
进程管理
woker 创建完成后,对请求的处理工作都会由 worker 进程来进行,而 master 进程负责对 worker 进程的监控和管理,比如 php-fpm reload 和 php-fpm stop 分别用来重新加载和停止 FPM。这部分工作是通过信号机制进行的,比如我们执行 reload 命令时,对主进程发送了 SIGUSR2 信号。下面我们对 PHP-FPM 中的 master 进程和 worker 进程的信号分别进行阐述。
首先说一下 master 进程的信号,其初始化工作是在 fpm_init 中实现的,具体函数为 fpm_signals_init_main,对应的代码如下:
int fpm_signals_init_main() /* {{{ */
{
struct sigaction act;
//创建管道并设置为非阻塞模式
if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()");
return -1;
}
if (0 > fd_set_blocked(sp[0], 0) || 0 > fd_set_blocked(sp[1], 0)) {
zlog(ZLOG_SYSERROR, "failed to init signals: fd_set_blocked()");
return -1;
}
//代码省略//
memset(&act, 0, sizeof(act));
act.sa_handler = sig_handler; //设置信号函数
sigfillset(&act.sa_mask);
//注册SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT信号
if (0 > sigaction(SIGTERM, &act, 0) ||
0 > sigaction(SIGINT, &act, 0) ||
0 > sigaction(SIGUSR1, &act, 0) ||
0 > sigaction(SIGUSR2, &act, 0) ||
0 > sigaction(SIGCHLD, &act, 0) ||
0 > sigaction(SIGQUIT, &act, 0)) {
zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
return -1;
}
return 0;
}
该函数主要做了两件事情。
-
创建了一个双向的管道 sp,并将其设置为非阻塞模式。
-
设置了 SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT 信号的回调函数 sig_handler,该函数的实现如下:
static void sig_handler(int signo) /* {{{ */
{
//对几种信号,使用char来表示
static const char sig_chars[NSIG + 1] = {
[SIGTERM] = 'T',
[SIGINT] = 'I',
[SIGUSR1] = '1',
[SIGUSR2] = '2',
[SIGQUIT] = 'Q',
[SIGCHLD] = 'C'
};
char s;
int saved_errno;
//保证是master进程
if (fpm_globals.parent_pid ! = getpid()) {
return;
}
saved_errno = errno;
s = sig_chars[signo];
//写入之前创建的管道的1端口
zend_quiet_write(sp[1], &s, sizeof(s));
errno = saved_errno;
}
从上述代码中,我们可以看到,当 master 进程收到信号时,会将其转换为对应的 char,然后将 char 写入管道的一端,那么谁读取呢?答案是 fpm_event_loop 函数,代码如下:
void fpm_event_loop(int err) /* {{{ */
{
static struct fpm_event_s signal_fd_event;
//保证是master进程
if (fpm_globals.parent_pid ! = getpid()) {
return;
}
//其中fpm_signals_get_fd()获取的是sp[0],注册的回调函数为fpm_got_signal
fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL);
从代码中可以看出,该函数从管道的另一端读取数据,并回调函数 fpm_got_signal。fpm_got_signal 的实现如下:
static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */
{
//代码省略//
do {
res = read(fd, &c, 1);
//代码省略//
switch (c) {
case 'C' : /* SIGCHLD */
//这个信号由worker进程发出,对相应的worker进程做一些善后工作
fpm_children_bury();
break;
case 'I' : /* SIGINT */
//收到SIGINT信号,master进程和worker进程退出
fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
break;
case 'T' : /* SIGTERM */
// 收到SIGTERM信号,master进程和worker进程退出
fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
break;
case 'Q' : /* SIGQUIT */
// 收到SIGQUIT信号,master进程和worker进程退出
fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET);
break;
case '1' : /* SIGUSR1 */
//代码省略//
//收到SIGUSR1信号,重新打开日志文件,并重启worker进程
ret = fpm_log_open(1);
//代码省略//
break;
case '2' : /* SIGUSR2 */
//重启worker进程
fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);
break;
}
//代码省略//
} while (1);
return;
}
从代码中可以看出以下几点。
-
对于 SIGCHLD 信号,该信号是由 worker 退出时发送的,master 进程收到这个信号后调用 fpm_children_bury 函数对 worker 进程进行善后工作;同时调用 fpm_children_make 函数按照不同模式启动 worker 进程。
-
对于 SIGUSR1 信号,调用的是 fpm_log_open 函数,重新打开日志文件,然后 fpm_pctl_kill_all 杀掉 worker 进程;这时候又会收到 SIGCHLD 信号,进行步骤1。
在大流量请求的情况下,切分日志时,会向 php-fpm 发送 SIGUSR1 信号,此时会有批量的 worker 进程被杀死,在重启完毕前,worker 进程数会瞬间变少,这时候会出现请求响应变慢的情况。
-
对于 SIGINT、SIGTERM、SIGQUIT 和 SIGUSR2 信号,调用的都是 fpm_pctl 函数,该函数有两个参数,第一个参数表示状态值,第二个参数表示操作类型,对应代码如下:
enum {
FPM_PCTL_STATE_UNSPECIFIED,
FPM_PCTL_STATE_NORMAL,
FPM_PCTL_STATE_RELOADING,
FPM_PCTL_STATE_TERMINATING,
FPM_PCTL_STATE_FINISHING
};
enum {
FPM_PCTL_ACTION_SET,
FPM_PCTL_ACTION_TIMEOUT,
FPM_PCTL_ACTION_LAST_CHILD_EXITED
};
收到 SIGUSR2 信号,执行 fpm_pctl_exec 函数,该函数内部调用 C 语言 execvp 函数启动 FPM。收到 SIGQUIT、SIGINT、SIGTREM 信号,执行 fpm_pctl_exit 函数实现主进程的退出。
到此我们了解了 master 进程对信号的处理工作,接下来我们讨论一下 worker 进程的信号处理,其实现是在函数中调用 fpm_signals_init_child,具体代码如下:
int fpm_signals_init_child() /* {{{ */
{
//代码省略//
act.sa_handler = &sig_soft_quit; //信号回调函数
act.sa_flags |= SA_RESTART;
act_dfl.sa_handler = SIG_DFL; //信号回调函数为SIG_DFL,默认处理
//关闭继承的管道的两端
close(sp[0]);
close(sp[1]);
if (0 > sigaction(SIGTERM, &act_dfl, 0) ||
0 > sigaction(SIGINT, &act_dfl, 0) ||
0 > sigaction(SIGUSR1, &act_dfl, 0) ||
0 > sigaction(SIGUSR2, &act_dfl, 0) ||
0 > sigaction(SIGCHLD, &act_dfl, 0) ||
0 > sigaction(SIGQUIT, &act, 0)) {
return -1;
}
//…省略代码…//
}
可以看出,SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD 的信号回调函数为 SIG_DFL,即默认处理;而 SIGQUIT 的信号回调函数为 sig_soft_quit,其实现如下:
static void sig_soft_quit(int signo) /* {{{ */
{
int saved_errno = errno;
close(0);
if (0 > socket(AF_UNIX, SOCK_STREAM, 0)) {
zlog(ZLOG_WARNING, "failed to create a new socket");
}
fpm_php_soft_quit();
errno = saved_errno;
}
void fpm_php_soft_quit() /* {{{ */
{
fcgi_terminate();
}
void fcgi_terminate(void)
{
in_shutdown = 1;
}
从代码中可以看出,该函数会将 in_shutdown 值设为 1,而 in_shutdown 控制子进程接收客户端请求操作,当 in_shutdown 等于 1 的时候,表明不再接收请求,则子进程会退出,关闭 CGI,释放资源等操作,做到了 “软” 关闭。
计分板
为了熟练地掌握各 woker 进程的工作情况,FPM 提供了一个计分板的功能,其核心结构体为 fpm_scoreboard_s 和 fpm_scoreboard_proc_s,具体定义如下:
struct fpm_scoreboard_s {
union { //保证原子性的锁机制
atomic_t lock;
char dummy[16];
};
char pool[32]; //worker名称
int pm; //运行模式
time_t start_epoch; //开始的时间
int idle; //process的空闲数
int active; //process的活跃数(工作中的)
int active_max; //最大活跃数
unsigned long int requests; //请求次数
unsigned int max_children_reached; // 达到最大进程数限制的次数
int lq; // 当前listen queue的请求数
int lq_max; //listen queue的大小
unsigned int lq_len; //listen queue的长度
unsigned int nprocs; //process的总数
int free_proc; //从process的列表遍历下一个空闲对象的开始下标
unsigned long int slow_rq; //慢请求数
struct fpm_scoreboard_proc_s *procs[]; //计分板详情
};
struct fpm_scoreboard_proc_s {
union { //保证原子性的锁机制
atomic_t lock;
char dummy[16];
};
int used; //是否被使用
time_t start_epoch;
pid_t pid; //进程id
unsigned long requests;
enum fpm_request_stage_e request_stage; //处理请求阶段,会在7.4.3节阐述
struct timeval accepted; //accept请求的时间
struct timeval duration; //脚本执行的时间
time_t accepted_epoch; //accept请求时间戳(秒)
struct timeval tv; //活跃时间
char request_uri[128]; //请求URI
char query_string[512]; //请求参数
char request_method[16]; //请求方法
size_t content_length; //请求长度
char script_filename[256];
char auth_user[32];
#ifdef HAVE_TIMES
struct tms cpu_accepted;
struct timeval cpu_duration;
struct tms last_request_cpu;
struct timeval last_request_cpu_duration;
#endif
size_t memory; //内存使用大小
};
从代码可以看出,fpm_scoreboard_s 结构记录 FPM 所有 worker 进程的汇总统计信息,而 fpm_scoreboard_proc_s 对应的是各 worker 进程的详细信息。FPM 提供了 3 个函数来统计计分。
-
fpm_scoreboard_update 函数:修改计分板里的各指标,为了保证原子性,使用了锁机制 fpm_spinlock,分别对两种 action 进行处理:
#define FPM_SCOREBOARD_ACTION_SET 0 //重置操作 #define FPM_SCOREBOARD_ACTION_INC 1 //增加操作
在 FastCGI 处理的每个阶段,调用该函数更新 worker 的计分板的数值。
-
fpm_scoreboard_proc_acquire函数:获取统计单元,调用的函数是 fpm_scoreboard_proc_get,这里也用到了锁机制,但是跟 update 对应的锁不一样。
-
fpm_scoreboard_proc_release 函数:与 acquire 对应,释放统计单元。
这 3 个函数如何使用呢?举个例子,在 FastCGI 读取 Header 阶段,调用函数 fpm_request_reading_headers:
void fpm_request_reading_headers() /* {{{ */
{
struct fpm_scoreboard_proc_s *proc;
//代码省略//
//获取统计单元
proc = fpm_scoreboard_proc_acquire(NULL, -1, 0);
//代码省略//
//修改统计单元信息
proc->request_stage = FPM_REQUEST_READING_HEADERS;
proc->tv = now;
proc->accepted = now;
proc->accepted_epoch = now_epoch;
#ifdef HAVE_TIMES
proc->cpu_accepted = cpu;
#endif
proc->requests++;
proc->request_uri[0] = '\0';
proc->request_method[0] = '\0';
proc->script_filename[0] = '\0';
proc->query_string[0] = '\0';
proc->auth_user[0] = '\0';
proc->content_length = 0;
//释放统计单元
fpm_scoreboard_proc_release(proc);
/* idle--, active++, request++ */
//更新计分板
fpm_scoreboard_update(-1, 1, 0, 0, 1, 0, 0, FPM_SCOREBOARD_ACTION_INC, NULL);
}
从代码中可以看出,不同的阶段会分别调用这 3 个函数来更新计分,这样可以准确地获知 worker 的运行状态,如空闲数、请求执行时间等,提供了监控系统健康状态的手段,图7-12 就是基于计分板绘制的 PHP-FPM 空闲数的图。
了解了进程创建的过程、进程的管理,以及对 worker 进程的计分策略,下面我们具体分析一下 worker 进程是如何工作的。
网络编程
Socket创建
calling process 进程调用 fpm_init 中的 fpm_unix_init_main 函数 fork 出 master 进程,master 进程调用 fpm_sockets_init_main 函数进行网络的监听,其具体实现如下:
/* create all required sockets */
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
switch (wp->listen_address_domain) {
case FPM_AF_INET :
wp->listening_socket = fpm_socket_af_inet_listening_socket(wp);
break;
case FPM_AF_UNIX :
if (0 > fpm_unix_resolve_socket_premissions(wp)) {
return -1;
}
wp->listening_socket = fpm_socket_af_unix_listening_socket(wp);
break;
}
//代码省略//
}
从代码中可以看出,在 Linux 中,Nginx 服务器和 PHP-FPM 可以通过 TCP Socket 和 UNIX Socket 两种方式实现。其中,UNIX Socket 是一种终端,可以使同一台操作系统上的两个或多个进程进行数据通信。这种方式需要在 Nginx 配置文件中填写 PHP-FPM 的 pid 文件位置,效率要比 TCP Socket 高。TCP Socket 的优点是可以跨服务器,当 Nginx 和 PHP-FPM 不在同一台机器上时,只能使用这种方式。配置方式如下:
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; ;
fastcgi_pass 127.0.0.1:9000; //TCP socket
#fastcgi_pass unix:/var/run/php7-fpm.sock; //UNIX socket
fastcgi_index index.php;
}
master 进程会创建 Socket,而 worker 进程会通过创建的 fd 来 accept 请求。
accept请求
根据上文的描述,FPM 的生命周期会进入循环中,代码如下:
zend_first_try {
//循环开始
while (EXPECTED(fcgi_accept_request(request) >= 0)) {//accept阻塞等待请求
//代码省略//
//初始化request
init_request_info();
fpm_request_info();
/** 代码省略,主要处理40x响应 **/
//解析FastCGI协议
fpm_request_executing();
//执行PHP脚本
php_execute_script(&file_handle);
fpm_request_end();
fpm_log_write(NULL);
php_request_shutdown((void *) 0);
//超过最大执行次数,退出
requests++;
if (UNEXPECTED(max_requests && (requests == max_requests))) {
cgi_finish_request(request, 1);
break;
}
/* 循环结束 */
}
从代码中可以非常清晰地看出,worker 进程会进入循环,当没有请求时,会阻塞在 fcgi_accept_request,让出 CPU 资源,成为空闲进程,当请求到达时会有一个 worker 进程抢到并处理,进入 FasCGI 的处理阶段,下面通过对 FastCGI 协议的阐述来理解 FPM 的工作。
FastCGI协议
FastCGI 是一种协议,它是建立在 CGI/1.1 基础之上的,把 CGI/1.1 里面要传递的数据通过 FastCGI 协议定义的顺序和格式进行传递。为了更好地理解 FPM 的工作,下面具体阐述一下 FastCGI 协议的内容。
消息类型
FastCGI 协议分为 10 种类型,具体定义如下:
typedef enum _fcgi_request_type {
FCGI_BEGIN_REQUEST = 1, /* [in] */
FCGI_ABORT_REQUEST = 2, /* [in] (not supported) */
FCGI_END_REQUEST = 3, /* [out] */
FCGI_PARAMS = 4, /* [in] environment variables */
FCGI_STDIN = 5, /* [in] post data */
FCGI_STDOUT = 6, /* [out] response */
FCGI_STDERR = 7, /* [out] errors */
FCGI_DATA = 8, /* [in] filter data (not supported) */
FCGI_GET_VALUES = 9, /* [in] */
FCGI_GET_VALUES_RESULT = 10 /* [out] */
} fcgi_request_type;
整个 FastCGI 是二进制连续传递的,定义了一个统一结构的消息头,用来读取每个消息的消息体,方便消息包的切割。一般情况下,最先发送的是 FCGI_BEGIN_REQUEST 类型的消息,然后是 FCGI_PARAMS 和 FCGI_STDIN 类型的消息,当 FastCGI 响应处理完后,将发送 FCGI_STDOUT 和 FCGI_STDERR 类型的消息,最后以 FCGI_END_REQUEST 表示请求的结束。FCGI_BEGIN_REQUEST 和 FCGI_END_REQUEST 分别表示请求的开始和结束,与整个协议相关。
消息头
以上 10 种类型的消息都是以一个消息头开始的,其结构体定义如下:
typedef struct _fcgi_header {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} fcgi_header;
其中:
-
version 标识 FastCGI 协议版本。
-
type 标识 FastCGI 记录类型。
-
requestId 标识消息所属的 FastCGI 请求,计算方式如下:
(requestIdB1 << 8) + requestIdB0
所以 requestId 的范围为 0~216-1,也就是 0~65535。
-
contentLength 是标识消息的 contentData 组件的字节数,计算方式跟 requestId 类似,范围同样是 0~65535。
(contentLengthB1 << 8) | contentLengthB0
-
paddingLength 是标识消息的 paddingData 组件的字节数,范围是 0~255;协议通过 paddingData 提供给发送者填充发送的记录的功能,并且方便接受者通过 paddingLength 快速地跳过 paddingData。填充的目的是允许发送者更有效地处理保持对齐的数据。如果内容的长度超过 65535 字节怎么办?答案是可以分成多个消息发送。
FCGI_BEGIN_REQUEST
FCGI_BEGIN_REQUEST 的结构体定义如下:
typedef struct _fcgi_begin_request {
unsigned char roleB1;
unsigned char roleB0;
unsigned char flags;
unsigned char reserved[5];
} fcgi_begin_request;
其中,role 代表的是 Web 服务器期望应用扮演的角色,计算方式如下:
(roleB1 << 8) + roleB0
PHP 7 处理了 3 种角色,分别是 FCGI_RESPONDER、FCGI_AUTHORIZER 和 FCGI_FILTER。
flags 和 FCGI_KEEP_CONN 如果为 0,则在对本次请求响应后关闭连接;如果非 0,则在对本次请求响应后不会关闭连接。
名-值对
对于 type 为 FCGI_PARAMS 类型,FastCGI 协议提供了名-值对来很好地满足读写可变长度的 name 和 value,格式如下:
nameLength+valueLength+name+value
为了节省空间,对于 0~127 长度的值,Length 使用了一个 char 来表示,第一位为 0,对于大于 127 的长度的值,Length 使用了 4 个 char 来表示,第一位为 1。具体如图7-15所示。

长度计算代码如下:
if (UNEXPECTED(name_len >= 128)) {
if (UNEXPECTED(p + 3 >= end)) return 0;
name_len = ((name_len & 0x7f) << 24);
name_len |= (*p++ << 16);
name_len |= (*p++ << 8);
name_len |= *p++;
}
这样可以表达 0~231 的长度。
请求协议
FastCGI 协议的定义结构体如下:
typedef struct _fcgi_begin_request_rec {
fcgi_header hdr;
fcgi_begin_request body;
} fcgi_begin_request_rec;
分析完 FastCGI 的协议,我们整体掌握了请求的 FastCGI 消息的内容,我们通过访问对应的接口,采用 gdb 抓取其中的内容。
首先我们修改 php-fpm.conf 的参数,保证只启动一个 worker:
pm.max_children = 1
重新启动 PHP-FPM:
./sbin/php-fpm -y etc/php-fpm.conf
对 worker 进行 gdb:
ps aux | grep php-fpm
root 30014 0.0 0.0142308 4724 ? Ss Nov26 0:03 php-fpm: master
process (etc/php-fpm.conf)
chenlei 30015 0.0 0.0142508 5500 ? S Nov26 0:00 php-fpm: pool www
gdb -p 30015
(gdb) b fcgi_read_request
通过浏览器访问 Nginx, Nginx 转发到 PHP-FPM 的 worker 上,根据 gdb 可以输出 FastCGI 消息的内容:
(gdb) b fcgi_read_request
对于第一个消息,内容如图7-16所示。

其中,type 对应的是 FCGI_BEGIN_REQUEST, requestid 为 1,长度为 8,恰好是 fcgi_begin_request 结构体的大小,内容如图7-17所示。

role 对应的是 FCGI_RESPONDER。继续往下读,得到的消息内容如图7-18所示。

其中,type 对应的是 FCGI_PARAMS, requestid 为 1,长度为
(contentLengthB1 << 8) | contentLengthB0 == 987
paddingLength=5,而 987+5=992,恰好是 8 的倍数。根据 contentLength+paddingLength 向后读取 992 长度的字节流,输出如下:
(gdb) p *p@987
$1 = "\017TSCRIPT_FILENAME/home/xiaoju/webroot/beatles/application/mis/mis/src/index.
php/admin/operation/index\f\016QUERY_STRINGactivity_id=89\016\003REQUEST_
METHODGET\f\000CONTENT_TYPE\016\000CONTENT_LENGTH\v SCRIPT_NAME/index.php/
admin/operation/index\v%REQUEST_URI/admin/operation/index? activity_id=89\f
DOCUMENT_URI/index.php/admin/operation/index\r4DOCUMENT_ROOT/home/xiaoju/
webroot/beatles/application/mis/mis/src\017\bSERVER_PROTOCOLHTTP/1.1\021\
aGATEWAY_INTERFACECGI/1.1\017\vSERVER_SOFTWAREnginx/1.2.5\v\rREMOTE_
A D D R172.22.32.131\v\005R E M O T E_P O R T50973\v\f S E R V E R_A D D R10.94.98.116\
v\004SERVER_PORT8085\v\000SERVER_NAME\017\003REDIRECT_STATUS200\t\021HTTP_
HOST10.94.98.116:8085\017\nHTTP_CONNECTIONkeep-alive\017xHTTP_USER_
AGENTMozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/62.0.3202.94 Safari/537.36\036\001HTTP_UPGRADE_INSECURE_
REQUESTS1\vUHTTP_ACCEPTtext/html, application/xhtml+xml, application/
xml; q=0.9, image/webp, image/apng, */*; q=0.8\024\rHTTP_ACCEPT_ENCODINGgzip,
deflate\024\027HTTP_ACCEPT_LANGUAGEzh-CN, zh; q=0.9, en; q=0.8"
根据名-值对的长度规则,我们可以看出,FastCGI 协议封装了类似于 HTTP 协议的键-值对。读取完毕后,继续跟踪消息,输出可以得出如图7-19所示的消息。

其中,type 对应的是 FCGI_PARAMS, requestid 为 1,长度为 0,此时完成了 FastCGI 协议消息的读取过程。下面介绍处理完请求后返回给 Nginx 的 FastCGI 协议的消息。
响应协议
fcgi_finish_request 调用 fcgi_flush, fcgi_flush 中封装一个 FCGI_END_REQUEST 消息体,再通过 safe_write 写入 Socket 连接的客户端描述符。
int fcgi_flush(fcgi_request *req, int close)
{
int len;
close_packet(req);
len = (int)(req->out_pos - req->out_buf);
if (close) {
fcgi_end_request_rec *rec = (fcgi_end_request_rec*)(req->out_pos);
//创建FCGI_END_REQUEST的头
fcgi_make_header(&rec->hdr, FCGI_END_REQUEST, req->id, sizeof(fcgi_end_
request));
//写入appStatus
rec->body.appStatusB3 = 0;
rec->body.appStatusB2 = 0;
rec->body.appStatusB1 = 0;
rec->body.appStatusB0 = 0;
//修改protocolStatus为FCGI_REQUEST_COMPLETE;
rec->body.protocolStatus = FCGI_REQUEST_COMPLETE;
len += sizeof(fcgi_end_request_rec);
}
if (safe_write(req, req->out_buf, len) ! = len) {
req->keep = 0;
req->out_pos = req->out_buf;
return 0;
}
req->out_pos = req->out_buf;
return 1;
}
到此我们就完全掌握了 FastCGI 协议。整个 FPM 模式实际上是多进程模式,首先由 calling process 进程 fork 出 master 进程,master 进程会创建 Socket,然后 fork 出 worker 进程,worker 进程会在 accept 处阻塞等待,请求过来时,由其中一个 worker 进程处理,按照 FastCGI 模式进行各阶段的读取,然后解析 PHP 并执行,最后按照 FastCGI 协议返回数据,继续进入 accept 处阻塞等待。另外,FPM 建立了计分板机制,可以关注全局和每个 woker 的工作情况,方便使用者监控。
除了 CLI 模式和 FPM 模式,还有很多其他基于 SAPI 的模式,下面我们简单介绍下。