守護進程在背景獨立於終端運行,不受使用者登出、終端關閉影響。典型特徵:無控制終端、在根目錄下運行、關閉標準輸入輸出、忽略終端信號。
# 背景執行,輸出導向 log.txt,忽略 SIGHUP nohup php worker.php > log.txt 2>&1 & # 查看進程 ps aux | grep worker.php # 停止 kill
優點:零代碼修改。缺點:無法在 PHP 內部控制、難以管理多進程、標準輸出需手動導向。
<?php
declare(ticks=1); // 讓信號處理器能在非阻塞操作中被調用
function daemonize(bool $redirectStd = true): void
{
// 第一次 fork:父進程退出,子進程成為孤兒進程,由 init(1) 收養
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('First fork failed');
} elseif ($pid > 0) {
exit(0); // 父進程退出,讓 shell 以為命令已結束
}
// 脫離終端:建立新 session,成為 session leader,失去控制終端
if (posix_setsid() === -1) {
throw new RuntimeException('posix_setsid failed');
}
// 第二次 fork:確保進程不是 session leader,無法重新獲得控制終端
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('Second fork failed');
} elseif ($pid > 0) {
exit(0); // 第一子進程退出,最終守護進程為第二子進程
}
// 可選:切換工作目錄到根目錄,避免掛載點無法卸載
chdir('/');
// 可選:重設 umask,確保建立檔案權限可控
umask(0);
// 可選:重導向標準輸入/輸出/錯誤到 /dev/null 或日誌檔
if ($redirectStd) {
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$null = fopen('/dev/null', 'r');
fopen('/dev/null', 'w'); // stdout
fopen('/dev/null', 'w'); // stderr
// 或導向日誌檔:
// $log = fopen('/var/log/mydaemon.log', 'a');
// $logErr = fopen('/var/log/mydaemon_error.log', 'a');
}
}
// 使用範例
daemonize();
// 註冊信號處理器
pcntl_signal(SIGTERM, function () {
// 優雅關閉:釋放資源、存檔狀態
syslog(LOG_INFO, 'Daemon received SIGTERM, shutting down...');
exit(0);
});
// 主迴圈
while (true) {
// 你的業務邏輯:監聽隊列、定時任務、Socket 服務等
doWork();
sleep(1);
}
function doWork(): void
{
// 實際工作內容
}
# /etc/systemd/system/php-worker.service [Unit] Description=PHP Background Worker After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/app ExecStart=/usr/bin/php /var/www/app/worker.php Restart=always RestartSec=5 StandardOutput=append:/var/log/php-worker.log StandardError=append:/var/log/php-worker_error.log [Install] WantedBy=multi-user.target
啟用:systemctl enable --now php-worker。優點:開機自啟、自動重啟、日誌集中、資源限制(MemoryLimit、CPUQuota)。
| 問題 | 原因 | 解決 |
|---|---|---|
| pcntl_fork 未定義 | 未安裝 pcntl 擴展或 Windows 環境 | Linux 安裝 php-pcntl;Windows 改用 nohup/Systemd/COM |
| 進程變殭屍進程 | 父進程未 wait 子進程 | 註冊 SIGCHLD handler 呼叫 pcntl_waitpid(-1, $status, WNOHANG) |
| 檔案描述符洩漏 | Fork 後子進程繼承父進程開啟的連線 | Fork 後在子進程重新建立 DB/Redis 連線 |
| 標準輸出卡住 | 管道滿未讀取 | 重導向到檔案或 /dev/null,或使用非阻塞 I/O |