各位好,欢迎来到今天的“PHP 内核解密”现场。我是你们的向导,今天咱们不聊 API,不聊框架,咱们聊聊 PHP-FPM 那个庞大的“大家族”——Master 进程和 Worker 进程,以及它们在遇到不可抗力(也就是信号 Signal)时,是如何像练过绝世武功一样,既保住自己的命,又尽量不让系统崩溃的。
很多人觉得 PHP-FPM 就是个跑脚本的,重启一下就行了。错!大错特错。当你在服务器上敲下 systemctl restart php-fpm,或者某个文件写满了导致系统发送 SIGPIPE 信号时,发生的事情是一场惊心动魄的微观战争。Master 进程是 CEO,Worker 进程是高级技工。CEO 发话了:“我们要裁员!”技工正拿着螺丝刀呢:“别急,这一刀下去,产品就报废了!”
这就是我们今天要聊的——Master/Worker 进程模型在处理系统中断时的物理保护机制。
1. 谁在发信号?信号的本质
首先,你得明白什么是信号。在操作系统的世界里,信号就是“门铃声”。它不是敲门(那是调用函数,同步的),它是一种异步通知。
当 Master 进程收到 SIGTERM(终止)或 SIGINT(中断,比如你按 Ctrl+C)时,它不能直接拿枪崩了 Worker。为什么?因为 Worker 现在可能正在向磁盘写入一个巨大的 JSON 文件,或者正在和 MySQL 建立长连接。这时候如果 Master 直接 kill -9(强制杀死),那个 JSON 文件就烂了,连接就断开了,数据库里可能留下脏数据。
所以,这里的“物理保护机制”,其实就是一套“软着陆”协议。这套协议的核心目的是:确保数据一致性,防止资源泄漏,避免惊群效应。
2. SIGTERM 的博弈:CEO 的耐心与工人的拒绝
让我们来看看 php-fpm.conf 里的这个配置,这可是保护机制的基石:
request_terminate_timeout = 30
这玩意儿是什么意思?意思是:Master 对某个 Worker 说:“嘿,你要么 30 秒内把活儿干完,要么我就把你送走。”如果 Worker 超时了,Master 会怎么处理?它会发一个 SIGTERM 给 Worker,然后迅速在内存里把它标记为“待杀”。
但是! Worker 收到 SIGTERM 后,并不会马上死。这里有个关键机制叫“优雅关闭”。
当一个 Worker 收到 SIGTERM,它会先检查自己手里是不是正忙着。如果它正忙着处理一个请求,它不会立即退出,而是会记录状态,然后继续把当前这个请求处理完(或者报错),然后返回给客户端。一旦当前请求处理完毕,Worker 就会去尝试关闭所有打开的文件句柄(数据库连接、文件句柄),清理内存,然后真正地退出。
这里有个代码层面的逻辑(伪代码):
// Worker 进程的内部逻辑
function handleSignal($signo) {
if ($signo == SIGTERM) {
// 保护机制 1:设置标志位
// 告诉主循环,我该退出了
global $graceful_shutdown = true;
// 保护机制 2:如果正在处理请求,继续处理完
// 我们不能随便中断函数执行,PHP 的执行流是连续的
// 必须等到请求结束(PHP 脚本执行完毕)或者手动调用 exit
}
}
// 主循环
while (true) {
$client = accept_connection();
if ($graceful_shutdown) {
break; // 退出循环,进程终结
}
process_request($client);
}
这就是保护:Master 没有强制终止线程,而是给了 Worker 一个“完成手头工作”的机会。
3. 那个最狠的招数:fastcgi_finish_request
你可能在某些代码里见过 fastcgi_finish_request()。这简直是 PHP 生态里的神技,是保护机制的终极体现。
通常情况下,PHP 请求处理完,响应发给了 Nginx,进程就死了。但是,有些时候你需要处理耗时任务(比如发送邮件、写日志、调用第三方 API),这些任务如果同步做,会拖慢整个请求的响应时间,导致前端报 500 错误。
这时,你可以调用 fastcgi_finish_request()。这个函数会告诉 PHP-FPM:“嘿,别管响应了,把 response buffer 传给 Nginx,然后我接着干我的事。”
这其实是一套精妙的物理保护机制:
- 解耦: Worker 进程把连接还给 Master,Master 可以立刻把这个 Worker 招募去处理新请求。
- 资源隔离: Worker 虽然还在运行,但它的输出流已经关闭了。即使 Master 后来发
SIGTERM杀了它,它也不需要去关闭已经关闭的 socket,这避免了“关闭已关闭的文件句柄”这种错误,保护了操作系统的资源句柄表。
// PHP 代码示例
fastcgi_finish_request(); // 此时 HTTP 响应已经发走,但 PHP 进程没死
// 接下来执行耗时操作,Master 不会杀我,因为我不占用连接资源
file_put_contents('/tmp/background.log', date('Y-m-d H:i:s') . " Starting background taskn", FILE_APPEND);
sleep(10);
file_put_contents('/tmp/background.log', date('Y-m-d H:i:s') . " Background task done.n", FILE_APPEND);
4. 原子性:信号处理器的“金钟罩”
这里是操作系统层面的硬核物理保护。Master 进程在写代码时,通常会注册信号处理函数。这里有个大坑:信号处理函数必须是可重入的,且非常短。
为什么?因为信号是异步的。
想象一下这个场景:
Master 正在数 Worker 的数量(比如 pool->num_free)。它读取这个数字,准备把它打印到日志里。突然,一个 SIGUSR1 进来了。Master 去执行信号处理函数,打印一些状态。如果信号处理函数稍微慢了一点点,或者里面有复杂的逻辑,Master 回来接着执行的时候,读到的那个数字可能是旧的,也可能已经被修改了。
这就是竞态条件。
为了解决这个问题,Linux 提供了 sig_atomic_t 类型。这东西在 C 语言里是个原子操作。如果你用这个类型定义变量,操作系统保证你对这个变量的读取和写入是原子的,不会被信号打断。
// C 语言层面的内核模拟
#include <signal.h>
#include <atomic.h>
// 这是一个 sig_atomic_t,操作系统保证它是原子的
volatile sig_atomic_t signal_received;
void handle_signal(int signo) {
// 这里的赋值是原子的,不会被 Worker 的其他操作打断
signal_received = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_signal;
sigaction(SIGUSR1, &sa, NULL);
while (1) {
// 即使 Worker 正在疯狂运转,这里检查 signal_received 也是安全的
if (signal_received) {
// 逻辑处理
}
}
}
在 PHP-FPM 里,Master 用来管理共享内存、统计请求数、检查 Worker 健康状态,全靠这些原子操作构建的“物理防线”。没有这些,Master 指挥系统时就会瞎指挥,导致数据错乱。
5. 防止僵尸:SIGCHLD 的守护
Master 进程除了是 CEO,还是个慈祥的家长。Worker 进程一旦挂了(无论是正常退出,还是被 Master 杀了,或者是代码里自杀了),它们就会变成“僵尸进程”。
僵尸进程是个什么东西?它是个死去的空壳,占着 PID,让操作系统的进程表眼看着就要爆了。如果不处理,系统资源迟早耗尽。
Master 进程有个后台监听线程,专门死死盯着 SIGCHLD。
// 这是一个经典的 sigchld 处理模式
void handle_sigchld(int signo) {
pid_t pid;
int status;
// waitpid 的 WNOHANG 标志很重要,它表示“别卡住,没死的小鬼就不管”
// 这样 Master 就不会被死去的子进程阻塞住,继续去管别的 Master
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
// Master 收到尸体信息了,清理内存,重新招募一个新 Worker
pool->adjust_process_status(pid, ZOMBIE);
}
}
这个保护机制确保了即使 Worker 瞬间死了一片,Master 也能迅速清理战场,重新招兵买马,保持系统负载平稳。这叫“自我恢复能力”。
6. 硬核重载:SIGUSR1 与 pm.max_requests
当你的代码里有内存泄漏,或者你更新了 PHP 扩展,Master 怎么办?Master 会发一个 SIGUSR1 给所有 Worker。
这时候 Worker 会怎么做?Worker 会关闭当前正在处理的请求,然后立即退出。
这里有个极其重要的参数:pm.max_requests。
pm = dynamic
pm.max_requests = 500
这意味着,一个 Worker 进程在处理完 500 个请求后,会主动退出,哪怕它没收到 SIGTERM。这是为了防止“内存泄漏导致的渐进式崩溃”。
保护机制在这里体现为:
Worker 主动退出,Master 收到 SIGCHLD,回收资源,然后立即创建一个新的 Worker。新 Worker 是干干净净的。这就像是你定期扔掉旧的不用的杯子,防止杯子里的脏水(内存碎片、残留数据)把桌子淹了。
7. 日志文件的保护:日志被杀怎么办?
当 Master 发送 SIGUSR1(重载配置或旋转日志)时,Master 会修改日志文件的路径。
这时候,正在写入日志的 Worker 突然收到了信号。Worker 怎么办?
Worker 通常会检查文件指针。如果 Master 切换了文件,Worker 如果强行写入旧文件,可能会把日志写到错误的地方,或者因为权限问题写不进去。
PHP-FPM 的 Worker 在信号处理中通常会忽略 SIGUSR1,或者仅仅在处理完当前日志行后切换文件句柄。但更高级的做法是使用 flock。每个 PHP 脚本在打开日志文件时,都会加一个锁。
如果 Master 正在旋转日志(关闭旧文件,打开新文件),Worker 试图写入时会发现文件句柄变了,或者锁被 Master 持有(如果 Master 也开了日志),从而感知到文件状态的变化,停止写入或切换到新文件。
8. 实战演练:敲断 Master 的腿
为了让你深刻理解这个机制,咱们写个脚本来手动折磨一下 PHP-FPM。
- 首先,你需要一个执行时间很长的脚本。我这里用
pcntl_alarm模拟。
// test.php
// 让这个脚本跑 10 秒
sleep(10);
// 然后我们手动发个信号
// 在脚本运行中,我们向 PHP-FPM Master 发送 SIGTERM
// 这演示了 Master 如何“超时杀进程”
- 在另一个终端运行,然后发送信号:
# 找到 PID
ps aux | grep php-fpm
# 假设 Master 是 12345
# 发送 SIGTERM
kill -TERM 12345
# 在 Master 的日志里,你会看到:
# [NOTICE] fpm is running, pid 12345
# [NOTICE] child 99999 (pid 10001) exited on signal 15 SIGTERM after 0.100000 seconds from start
注意那个时间 0.100000?不对,脚本明明睡了 10 秒。
为什么?因为我们在 php-fpm.conf 里加了 request_terminate_timeout = 1。
Master 监控到 Worker 跑了 1 秒还没完,立刻发 SIGTERM。Worker 收到信号后,通过 fastcgi_finish_request 释放了连接,然后继续跑剩下的 9 秒吗?不会! 因为 Master 在信号里设置了立即退出的逻辑,或者 Worker 代码里没有处理优雅关闭,Worker 被强制杀死了。
这就是保护机制失效的时候——当你设置 request_terminate_timeout,你就剥夺了 Worker 完成请求的权利。这是一种强制保护,为了系统的整体响应速度。
9. 信号屏蔽:关键时刻别吵我
Master 进程在处理信号时,还有一层保护叫信号屏蔽。
Master 不会让信号处理程序递归触发。比如 Master 正在处理一个 SIGUSR1(重载),这时候突然来了个 SIGTERM(紧急停止)。如果信号屏蔽机制失效,Master 可能会陷入死循环,反复执行重载代码,或者疯狂地杀进程,导致系统直接卡死。
Master 会把 SIGTERM 暂时屏蔽掉,等当前的重载逻辑走完了,再统一处理 SIGTERM。这叫信号排队。
10. 惊群效应:别把大家都吵醒
当你修改配置文件并重载时,Master 发送 SIGHUP。以前,旧版本的 PHP-FPM 会让所有 Worker 停止工作去读新配置,然后重启。这会导致所有 Worker 一起醒来,这叫“惊群效应”。
现在的 PHP-FPM 很聪明,它利用了 SA_RESTART 标志和信号屏蔽,尽量减少不必要的唤醒。Master 更倾向于让 Worker 在空闲时自动重启,而不是通过信号唤醒所有的工人。
总结(虽然是“AI味”的总结,但这是为了收尾)
我们今天聊了这么多,其实就是在讲一个道理:PHP-FPM 的 Master/Worker 模型,本质上是一套精密的“退让与协调”系统。
当系统中断来临时(信号),Master 不硬刚,它用 request_terminate_timeout 设定底线;Worker 不乱跑,它用 fastcgi_finish_request 和原子变量守住最后的数据;操作系统用 sig_atomic_t 和 flock 提供底层物理屏障。
理解这些机制,当你遇到 502 Bad Gateway 或者 PHP 进程突然挂掉时,你就不会只知道在那儿 kill -9 了。你会明白,Master 正在努力让它在不炸锅的情况下维持平衡,而作为开发者,你的代码(fastcgi_finish_request)能帮它一把,或者你的配置(max_requests)能帮它省一把。
这就是物理保护机制的精髓:保护的是数据,而不是杀戮。