守護進程是長期在後台運行、無控制終端的特殊進程,常用於任務隊列消費、定時任務調度、WebSocket 服務端等場景。PHP 配合 PCNTL 和 POSIX 擴展可完整實現守護進程功能。本文系統梳理核心原理、關鍵步驟,並提供生產可用的完整類庫實現。
pcntl_fork() 父進程退出,子進程繼續。posix_setsid() 成為會話組長,脫離原控制終端。chdir('/') 避免掛載點無法卸載。/dev/null 或日誌文件。umask(0) 確保創建文件權限可控。<?php
/**
* 生產級 PHP 守護進程基類
* 支援單例運行、多工作進程、信號處理、優雅關閉
*/
class DaemonCommand {
private $pidFile = '';
private $terminate = false;
private $workersCount = 0;
private $workersMax = 8;
private $jobs = [];
private $isSington = false;
private $user = 'nobody';
private $output = '/dev/null';
public function __construct($isSington = false, $user = 'nobody', $output = '/dev/null') {
$this->isSington = $isSington;
$this->user = $user;
$this->output = $output;
$this->checkEnvironment();
}
/** 檢查 PCNTL/POSIX 擴展 */
private function checkEnvironment() {
if (!function_exists('pcntl_signal')) {
throw new Exception('PHP 必須編譯 PCNTL 擴展');
}
if (version_compare(PHP_VERSION, '5.3', '>=')) {
declare(ticks = 1);
}
// 註冊信號處理
pcntl_signal(SIGTERM, [$this, 'signalHandler'], false);
pcntl_signal(SIGINT, [$this, 'signalHandler'], false);
pcntl_signal(SIGQUIT, [$this, 'signalHandler'], false);
pcntl_signal(SIGCHLD, [$this, 'signalHandler'], false);
if (function_exists('gc_enable')) gc_enable();
}
/** 守護進程化核心流程 */
public function daemonize() {
if (php_sapi_name() !== 'cli') die('僅支援 CLI 模式運行');
// 單例檢查
if ($this->isSington) {
$this->pidFile = sys_get_temp_dir() . '/daemon_' . basename($_SERVER['argv'][0], '.php') . '.pid';
$this->checkPidFile();
}
umask(0);
// 第一次 fork
if (pcntl_fork() !== 0) exit(0);
// 建立新會話
if (!posix_setsid()) throw new Exception('setsid 失敗');
// 第二次 fork
if (pcntl_fork() !== 0) exit(0);
chdir('/');
$this->setUser($this->user) or die('切換用戶失敗');
// 關閉標準流並重定向
fclose(STDIN); fclose(STDOUT); fclose(STDERR);
$stdin = fopen($this->output, 'r');
$stdout = fopen($this->output, 'a');
$stderr = fopen($this->output, 'a');
if ($this->isSington) $this->createPidFile();
}
/** 啟動主循環 */
public function start($workerCount = 1) {
$this->log('守護進程啟動,工作進程數: ' . $workerCount);
$this->workersMax = $workerCount;
while (true) {
if (function_exists('pcntl_signal_dispatch')) pcntl_signal_dispatch();
if ($this->terminate) break;
if ($this->workersCount < $workerCount) {
$pid = pcntl_fork();
if ($pid > 0) {
$this->workersCount++;
} elseif ($pid === 0) {
// 子進程:恢復默認信號處理,執行任務
pcntl_signal(SIGTERM, SIG_DFL);
pcntl_signal(SIGCHLD, SIG_DFL);
$this->runJob();
exit(0);
}
} else {
sleep(2); // 滿載等待
}
}
$this->shutdown();
}
/** 子進程任務邏輯 */
private function runJob() {
if (empty($this->jobs)) return;
$job = $this->jobs;
while ($job['runtime']--) {
call_user_func($job['function'], $job['argv'] ?? null);
sleep(2);
}
}
/** 設置任務 */
public function setJob(array $job) {
$job += ['argv' => '', 'runtime' => 1];
if (empty($job['function'])) throw new Exception('必須指定執行函數');
$this->jobs = $job;
}
/** 信號處理器 */
public function signalHandler($signo) {
switch ($signo) {
case SIGCHLD:
while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
$this->workersCount--;
}
break;
case SIGTERM:
case SIGHUP:
case SIGQUIT:
$this->terminate = true;
break;
}
}
/** 關閉清理 */
private function shutdown() {
if ($this->pidFile && file_exists($this->pidFile)) {
unlink($this->pidFile);
$this->log('PID 文件已刪除');
}
$this->log('守護進程正常退出');
posix_kill(0, SIGKILL);
exit(0);
}
/** 輔助方法 */
private function checkPidFile() { /* 檢查 PID 文件是否存在且進程存活 */ }
private function createPidFile() { /* 寫入當前 PID */ }
private function setUser($name) { /* 切換運行用戶 */ }
private function log($msg) { printf("%s\t%d\t%s\n", date('c'), posix_getpid(), $msg); }
}
// 使用示例
$daemon = new DaemonCommand(true, 'www-data', '/var/log/daemon.log');
$daemon->daemonize();
$daemon->setJob([
'function' => function() { /* 具體業務邏輯 */ },
'argv' => '',
'runtime' => 0 // 0 表示無限循環
]);
$daemon->start(4); // 啟動 4 個工作進程
?>
Restart=always 實現自動拉起。mysqli::reconnect()。pcntl_signal_dispatch() 輪詢而非 declare(ticks=1)。