守護進程在後台長期運行,脫離控制終端,周期性執行任務或等待事件。典型場景:隊列消費者、定時同步、Socket 服務端。
wait() 回收fclose(STDIN/STDOUT/STDERR),重定向到 /dev/null 或日誌文件pcntl_signal(SIGCHLD, SIG_IGN) 讓內核自動回收僵屍進程<?php
class DaemonCommand {
private $pidFile, $workersMax = 8, $workersCount = 0, $terminate = false;
public function __construct($singleton = true, $user = 'nobody', $output = '/dev/null') {
$this->pidFile = sys_get_temp_dir().'/'.__CLASS__.'.pid';
$this->checkPcntl();
}
private function checkPcntl() {
if (!function_exists('pcntl_signal')) throw new Exception('Need PCNTL extension');
declare(ticks=1); // PHP < 5.3
pcntl_signal(SIGTERM, [$this, 'signalHandler']);
pcntl_signal(SIGINT, [$this, 'signalHandler']);
pcntl_signal(SIGQUIT, [$this, 'signalHandler']);
pcntl_signal(SIGCHLD, [$this, 'signalHandler']);
}
public function daemonize() {
if (php_sapi_name() !== 'cli') die('CLI only');
if (pcntl_fork() !== 0) exit; // 1st fork
posix_setsid(); // setsid
if (pcntl_fork() !== 0) exit; // 2nd fork
chdir('/');
posix_setuid(posix_getpwnam('nobody')['uid']);
fclose(STDIN); fclose(STDOUT); fclose(STDERR);
$null = fopen('/dev/null','r'); fopen('/dev/null','a'); fopen('/dev/null','a');
file_put_contents($this->pidFile, posix_getpid());
}
public function signalHandler($signo) {
switch($signo) {
case SIGUSR1: if($this->workersCount < $this->workersMax) { $this->forkWorker(); } break;
case SIGCHLD: while(($pid=pcntl_waitpid(-1,$s,WNOHANG))>0) $this->workersCount--; break;
case SIGTERM: case SIGQUIT: $this->terminate = true; break;
}
}
public function start($count=1, $job) {
$this->daemonize();
while(!$this->terminate) {
pcntl_signal_dispatch();
if($this->workersCount < $count) $this->forkWorker($job);
sleep(1);
}
$this->shutdown();
}
private function forkWorker($job) {
$pid = pcntl_fork();
if($pid > 0) { $this->workersCount++; return; }
if($pid === 0) {
pcntl_signal(SIGTERM, SIG_DFL); pcntl_signal(SIGCHLD, SIG_DFL);
call_user_func($job);
exit(0);
}
}
private function shutdown() {
@unlink($this->pidFile);
posix_kill(0, SIGKILL);
}
}
// 使用範例
$daemon = new DaemonCommand();
$daemon->start(4, function(){ /* 你的任務邏輯 */ sleep(2); });
Type=forking + PIDFile=,實現開機自啟、崩潰自動重啟logrotateSIGUSR1 觸發 Worker 重載代碼,或 SIGTERM 觸發主進程關閉ps aux | grep daemon、Prometheus 推送指標| 現象 | 原因 | 修正 |
|---|---|---|
| 進程僵屍 | 未忽略 SIGCHLD | pcntl_signal(SIGCHLD, SIG_IGN) |
| 無法寫入日誌 | 工作目錄在掛載點 | chdir('/') 或指定絕對路徑 |
| 多實例衝突 | PID 文件未鎖定 | 啟動時 flock() 檢查 |
PHP 寫 Daemon 核心在於 雙 fork + setsid + 信號處理 + PCNTL。建議將上述類封裝為 Composer 包,業務層只需關注 $job 閉包。生產環境務必配合 Systemd 管理,而非直接 nohup php daemon.php &。