首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 守護進程(Daemon)編寫完整指南

     編輯:PHP 綜合知識

什麼是守護進程?

守護進程在後台長期運行,脫離控制終端,周期性執行任務或等待事件。典型場景:隊列消費者、定時同步、Socket 服務端。

Linux 進程模型關鍵概念

  • 進程組(PGID):組長 PID = PGID
  • 會話(SID):會話組長 = 進程組長,獨佔控制終端
  • 孤兒進程:父進程退出,由 init (PID 1) 收養
  • 僵屍進程:子進程結束,父進程未 wait() 回收

七步守護化標準流程

  1. fork() 並讓父進程退出 → 子進程成為孤兒,進入後台
  2. setsid() → 脫離原會話/終端,成為新會話組長
  3. 再次 fork() 並結束第一子進程 → 二代子進程不再是會話組長,無法重新申請控制終端
  4. chdir("/") → 釋放掛載點,避免文件系統無法卸載
  5. umask(0) → 清除繼承的文件創建掩碼
  6. 關閉標準輸入/輸出/錯誤 → fclose(STDIN/STDOUT/STDERR),重定向到 /dev/null 或日誌文件
  7. 處理 SIGCHLD:pcntl_signal(SIGCHLD, SIG_IGN) 讓內核自動回收僵屍進程

完整可用的 Daemon 類(支援多 Worker、信號控制、PID 文件鎖)

<?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); });

部署與管理技巧

  • Systemd 服務單元:Type=forking + PIDFile=,實現開機自啟、崩潰自動重啟
  • 日誌輪轉:輸出到文件,配合 logrotate
  • 優雅重啟:發送 SIGUSR1 觸發 Worker 重載代碼,或 SIGTERM 觸發主進程關閉
  • 監控:ps aux | grep daemon、Prometheus 推送指標

常見坑點

現象原因修正
進程僵屍未忽略 SIGCHLDpcntl_signal(SIGCHLD, SIG_IGN)
無法寫入日誌工作目錄在掛載點chdir('/') 或指定絕對路徑
多實例衝突PID 文件未鎖定啟動時 flock() 檢查

總結

PHP 寫 Daemon 核心在於 雙 fork + setsid + 信號處理 + PCNTL。建議將上述類封裝為 Composer 包,業務層只需關注 $job 閉包。生產環境務必配合 Systemd 管理,而非直接 nohup php daemon.php &。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved