PHP怎么写守护进程脚本

wen PHP项目 1

PHP守护进程脚本实战指南:从零到生产级部署

目录导读

  1. 为什么PHP需要守护进程? —— 场景与痛点分析
  2. 基础原理 —— fork、POSIX与信号处理
  3. 手写一个最小守护进程 —— 代码逐行拆解
  4. 生产级进阶 —— 日志、重启、防止多实例
  5. 常见问题FAQ —— 崩溃、僵尸进程与性能
  6. 推荐工具与替代方案 —— 从pcntl到Swoole/Workerman

为什么PHP需要守护进程?

PHP常被看作“请求-响应”型语言,但很多业务场景(如队列消费、实时推送、定时任务调度)需要常驻内存的进程,举个例子:你用cron每分钟跑一次php send_email.php,但如果任务耗时5分钟,就会出现并发叠加,引发重复发送邮件的问题,守护进程则能长期驻留,顺序消费队列,彻底解决这类并发冲突。

PHP怎么写守护进程脚本

核心优势

  • 内存复用(加载的类、连接池不释放)
  • 避免重复初始化开销
  • 支持平滑重启和信号控制

基础原理: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 自动重启与看护

结合Supervisorsystemd,以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 PIDkill -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
复杂进程管理 Supervisorsystemd

写在最后:写守护进程不难,难的是稳定性和可观测性,建议生产环境务必加日志记录、健康检查(比如定期更新心跳文件)、以及内存监控,这样即使出现故障,也能第一时间定位。


延伸思考:如果你的业务是异步任务,是否可以直接用Redis + Laravel Queue 替代守护进程?这能大幅降低维护成本——很多场景下,框架自带的队列系统已经提供了重试、超时等机制,远比手写守护进程更可靠。

抱歉,评论功能暂时关闭!