PHP守护进程脚本实战指南:从零到生产级部署
目录导读
- 为什么PHP需要守护进程? —— 场景与痛点分析
- 基础原理 —— fork、POSIX与信号处理
- 手写一个最小守护进程 —— 代码逐行拆解
- 生产级进阶 —— 日志、重启、防止多实例
- 常见问题FAQ —— 崩溃、僵尸进程与性能
- 推荐工具与替代方案 —— 从pcntl到Swoole/Workerman
为什么PHP需要守护进程?
PHP常被看作“请求-响应”型语言,但很多业务场景(如队列消费、实时推送、定时任务调度)需要常驻内存的进程,举个例子:你用cron每分钟跑一次php send_email.php,但如果任务耗时5分钟,就会出现并发叠加,引发重复发送邮件的问题,守护进程则能长期驻留,顺序消费队列,彻底解决这类并发冲突。

核心优势:
- 内存复用(加载的类、连接池不释放)
- 避免重复初始化开销
- 支持平滑重启和信号控制
基础原理:PCNTL扩展与POSIX信号
PHP要实现守护进程,必须依赖pcntl(进程控制)和posix(POSIX系统接口)扩展,检查你的环境:
php -m | grep pcntl php -m | grep posix
如果没有,编译时加上--enable-pcntl --enable-posix(Linux下),Windows环境不支持,建议使用Docker或虚拟机。
关键函数:
pcntl_fork():创建子进程posix_setsid():创建新会话,脱离终端pcntl_signal():注册信号处理器pcntl_wait():等待子进程退出
手写一个最小守护进程
下面代码是经典实现,注释已逐行解释:
<?php
declare(ticks=1); // 信号处理必须开启
// 1. 创建子进程
$pid = pcntl_fork();
if ($pid == -1) {
exit("fork失败\n");
} elseif ($pid > 0) {
// 父进程退出,让子进程成为孤儿,由init接管
exit(0);
}
// 2. 子进程创建新会话,脱离终端
if (posix_setsid() == -1) {
exit("setsid失败\n");
}
// 3. 再次fork,防止重新获取控制终端
$pid2 = pcntl_fork();
if ($pid2 == -1) {
exit("第二次fork失败\n");
} elseif ($pid2 > 0) {
exit(0);
}
// 4. 改变工作目录 & 文件掩码
chdir("/");
umask(0);
// 5. 重定向标准输入输出到/dev/null
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$stdin = fopen('/dev/null', 'r');
$stdout = fopen('/var/log/daemon.log', 'a');
$stderr = fopen('/var/log/daemon_err.log', 'a');
// 6. 主循环
while (true) {
// 你的业务逻辑,比如消费队列
file_put_contents('/tmp/daemon_ping.log', date('Y-m-d H:i:s')."\n", FILE_APPEND);
sleep(5);
}
运行测试:
php daemon.php ps aux | grep daemon.php
你会发现进程常驻,并每5秒记录一条日志。
生产级进阶:健壮性设计
1 信号处理与平滑退出
pcntl_signal(SIGTERM, 'shutdown');
pcntl_signal(SIGINT, 'shutdown');
function shutdown($signo) {
// 清理资源,如关闭数据库连接
exit(0);
}
这样你就可以通过kill -TERM 进程ID优雅停止进程。
2 防止多实例运行
用PID文件锁:
$pidFile = '/tmp/my_daemon.pid';
if (file_exists($pidFile)) {
$pid = file_get_contents($pidFile);
if (posix_kill($pid, 0)) {
exit("已有一个实例运行,PID: $pid\n");
}
}
file_put_contents($pidFile, getmypid());
3 自动重启与看护
结合Supervisor或systemd,以systemd为例,创建/etc/systemd/system/my-daemon.service:
[Unit] Description=My PHP Daemon [Service] ExecStart=/usr/bin/php /var/www/daemon.php Restart=always User=www-data [Install] WantedBy=multi-user.target
然后systemctl enable my-daemon,崩溃后自动拉起。
4 内存泄漏监控
常驻进程最怕内存涨,在循环里加:
每个周期输出memory_get_usage(true),配合警报。
常见问题FAQ
Q1:为什么我的守护进程无法用Ctrl+C停止?
A:因为你已经通过setsid脱离了终端,信号不再由终端传递,需要使用kill -9 PID或kill -TERM PID。
Q2:守护进程创建后,终端关闭就挂掉?
A:这通常是因为你没有正确重定向STDIN/STDOUT/STDERR,确保三个流都指向文件或/dev/null。
Q3:子进程变成僵尸进程(Z)怎么办?
A:在父进程中调用pcntl_waitpid($pid, $status, WNOHANG)回收,或设置pcntl_signal(SIGCHLD, SIG_IGN)。
Q4:Swoole/Workerman和原生PCNTL有什么区别? A:Swoole提供了更高级的协程和进程管理,底层是C实现,性能更强,适合高并发网络服务,而PCNTL是基础,更轻量,适合简单的队列消费。
推荐工具与替代方案
| 场景 | 推荐方案 |
|---|---|
| 简单队列消费 | 原生PCNTL + Redis队列 |
| 定时任务调度 | Cron + Laravel Scheduler |
| 长连接/WebSocket服务 | Swoole / Workerman |
| 复杂进程管理 | Supervisor 或 systemd |
写在最后:写守护进程不难,难的是稳定性和可观测性,建议生产环境务必加日志记录、健康检查(比如定期更新心跳文件)、以及内存监控,这样即使出现故障,也能第一时间定位。
延伸思考:如果你的业务是异步任务,是否可以直接用Redis + Laravel Queue 替代守护进程?这能大幅降低维护成本——很多场景下,框架自带的队列系统已经提供了重试、超时等机制,远比手写守护进程更可靠。