PHP Swoole 守护进程开发实例:后台常驻与线程池处理
本文讲解如何利用 PHP 的 Swoole 扩展创建高性能守护进程。内容涵盖进程隔离、PID 文件管理、线程池并发处理以及非特权用户运行等核心步骤,帮助开发者构建稳定可靠的后台服务。
为什么需要开发守护进程
守护进程是脱离于终端并在后台运行的进程。其核心价值在于:
- 避免干扰: 不受终端信息显示或键盘输入的影响,防止意外中断。
- 持久运行: 以服务模式存在,长期监听 TCP/UDP Socket、UNIX Socket 等接口,持续处理数据。
- 资源隔离: 独立于前台会话,适合运行数据库同步、消息队列消费等长时间任务。
开发前准备条件
要编写一个健壮的 PHP 守护进程,需满足以下条件:
- Swoole 扩展: 必须安装支持多线程功能的 Swoole 版本。
- 依赖库: 示例中使用
ZMQ进行通信,需安装相应的 ZeroMQ PHP 扩展。 - 环境权限: 为安全起见,建议以非 root 用户运行,并配置专用的 UID/GID。
- 冲突规避: 实现单实例运行机制,防止因重复启动导致端口冲突或资源浪费。
核心代码结构解析
完整的守护进程通常包含工作线程类、任务数据类和主控类三个部分。以下是一个基于 Swoole Pool 和 ZeroMQ 的实现示例。
<?php
class ExampleWorker extends Worker {
// 数据库连接句柄,供子任务复用
protected static $dbh;
public function __construct() {}
public function run(){
// 在子线程中初始化数据库连接
$dbhost = '192.168.2.1';
$dbport = 3306;
$dbuser = 'www';
$dbpass = 'qwer123';
$dbname = 'example';
self::$dbh = new PDO("mysql:host=$dbhost;port=$dbport;dbname=$dbname", $dbuser, $dbpass, array(
PDO::MYSQL_ATTR_COMPRESS => true,
PDO::ATTR_PERSISTENT => true
));
}
protected function getInstance(){
return self::$dbh;
}
}
// 可收集的任务类,用于 Pool::collect 回收
/* the collectable class implements machinery for Pool::collect */
class Fee extends Stackable {
public function __construct($msg) {
// 解析传入的消息
$trades = explode(",", $msg);
$this->data = $trades;
print_r($trades);
}
public function run() {
try {
$dbh = $this->worker->getInstance();
// 执行插入操作
$insert = "INSERT INTO fee(ticket, login, volume, `status`) VALUES(:ticket, :login, :volume,'N')";
$sth = $dbh->prepare($insert);
$sth->bindValue(':ticket', $this->data[0]);
$sth->bindValue(':login', $this->data[1]);
$sth->bindValue(':volume', $this->data[2]);
$sth->execute();
$sth = null;
/* ...... */
// 更新状态
$update = "UPDATE fee SET `status` = 'Y' WHERE ticket = :ticket and `status` = 'N'";
$sth = $dbh->prepare($update);
$sth->bindValue(':ticket', $this->data[0]);
$sth->execute();
} catch(PDOException $e) {
// 错误日志记录
$error = sprintf("%s,%s\n", $mobile, $id );
file_put_contents("mobile_error.log", $error, FILE_APPEND);
}
}
}
class Example {
// 配置项
const LISTEN = "tcp://192.168.2.15:5555";
const MAXCONN = 100;
const pidfile = __CLASS__;
const uid = 80;
const gid = 80;
protected $pool = NULL;
protected $zmq = NULL;
public function __construct() {
$this->pidfile = '/var/run/'.self::pidfile.'.pid';
}
private function daemon(){
// 检查 PID 文件,防止重复启动
if (file_exists($this->pidfile)) {
echo "The file $this->pidfile exists.\n";
exit();
}
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// 父进程退出
exit($pid);
} else {
// 子进程进入后台
file_put_contents($this->pidfile, getmypid());
// 降低权限至指定用户
posix_setuid(self::uid);
posix_setgid(self::gid);
return(getmypid());
}
}
private function start(){
$pid = $this->daemon();
// 创建线程池
$this->pool = new Pool(self::MAXCONN, \ExampleWorker::class, []);
// 绑定 ZeroMQ Socket
$this->zmq = new ZMQSocket(new ZMQContext(), ZMQ::SOCKET_REP);
$this->zmq->bind(self::LISTEN);
// 循环接收并分发任务
while ($message = $this->zmq->recv()) {
$this->pool->submit(new Fee($message));
$this->zmq->send('TRUE');
}
$this->pool->shutdown();
}
private function stop(){
if (file_exists($this->pidfile)) {
$pid = file_get_contents($this->pidfile);
posix_kill($pid, 9);
unlink($this->pidfile);
}
}
private function help($proc){
printf("%s start | stop | help \n", $proc);
}
public function main($argv){
if(count($argv) < 2){
printf("please input help parameter\n");
exit();
}
if($argv[1] === 'stop'){
$this->stop();
}else if($argv[1] === 'start'){
$this->start();
}else{
$this->help($argv[0]);
}
}
}
$cgse = new Example();
$cgse->main($argv);
第1步:实现后台驻留 (Fork)
目的是让程序脱离当前终端窗口,在后台持续运行。通过 pcntl_fork() 创建子进程,父进程直接退出,子进程写入 PID 文件并切换用户身份。
private function daemon(){
// 1. 防重入检查
if (file_exists($this->pidfile)) {
echo "The file $this->pidfile exists.\n";
exit();
}
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// 父进程退出
exit($pid);
} else {
// 2. 子进程执行
// 将当前子进程的 PID 写入文件
file_put_contents($this->pidfile, getmypid());
// 3. 权限降级:从 root 切换到普通用户 (如 www)
posix_setuid(self::uid);
posix_setgid(self::gid);
return(getmypid());
}
}
完成判断: 执行后,当前终端无阻塞,且 /var/run/ 目录下生成了对应的 .pid 文件。
第2步:启动服务与线程池
在主进程中初始化 Swoole 线程池和 ZeroMQ 监听器,进入死循环等待数据。
- 初始化池:
$this->pool = new Pool(self::MAXCONN, \ExampleWorker::class, []); - 绑定监听:
$this->zmq->bind(self::LISTEN); - 任务分发: 循环调用
$this->zmq->recv()获取消息,并通过$this->pool->submit(new Fee($message))提交给线程池处理。
第3步:优雅停止服务
停止脚本需读取 PID 文件,发送信号终止进程,并清理 PID 文件。
private function stop(){
if (file_exists($this->pidfile)) {
$pid = file_get_contents($this->pidfile);
// 强制杀死进程
posix_kill($pid, 9);
unlink($this->pidfile);
}
}
注意事项与排查
- 权限问题: 若以 root 运行,子进程应主动
posix_setuid降权,防止漏洞被利用时获得最高权限。 - PID 文件残留: 若程序非正常退出(如断电),PID 文件可能未被删除。此时手动执行
unlink或rm命令清除该文件后再启动。 - 僵尸进程: 上述示例中父进程直接
exit,若需更严格的管理,可取消注释pcntl_wait($status)来回收子进程。
以上就是PHP高级编程编写守护进程的详细内容,更多关于PHP后端开发技术的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













