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

PHP 守護進程開發全攻略:從原理到生產級實現

     編輯:PHP 相關知識

PHP 守護進程開發全攻略:從原理到生產級實現

守護進程是長期在後台運行、無控制終端的特殊進程,常用於任務隊列消費、定時任務調度、WebSocket 服務端等場景。PHP 配合 PCNTL 和 POSIX 擴展可完整實現守護進程功能。本文系統梳理核心原理、關鍵步驟,並提供生產可用的完整類庫實現。

核心概念速覽

  • 進程與進程組:每個進程屬於一個進程組(PGID = 組長 PID),組長退出不影響組員,但組員成為孤兒進程會被 init 進程收養。
  • 會話與控制終端:會話包含多個進程組,共享一個控制終端。守護進程必須脫離控制終端,避免終端關閉導致 SIGHUP 信號終止進程。
  • 雙重 Fork 機制:第一次 fork 讓父進程退出,子進程成為背景進程;第二次 fork 確保進程不是會話組長,防止重新獲得控制終端。

守護進程標準化七步法

  1. 後台化:pcntl_fork() 父進程退出,子進程繼續。
  2. 建立新會話:posix_setsid() 成為會話組長,脫離原控制終端。
  3. 二次 Fork:再次 fork,父進程退出,子進程不再是會話組長,徹底失去開啟控制終端能力。
  4. 切換工作目錄:chdir('/') 避免掛載點無法卸載。
  5. 關閉標準流:關閉 STDIN/STDOUT/STDERR,重定向到 /dev/null 或日誌文件。
  6. 重設文件掩碼:umask(0) 確保創建文件權限可控。
  7. 處理信號:設置 SIGCHLD 忽略避免殭屍進程,捕獲 SIGTERM/SIGINT 實現優雅關閉。

生產級守護進程類庫實現

<?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 個工作進程
?>

部署與運維要點

  • 系統服務化:配合 systemd 管理,配置 Restart=always 實現自動拉起。
  • 日誌切割:使用 logrotate 定期切割日誌,避免磁盤佔滿。
  • 監控指標:暴露進程數、任務處理耗時、錯誤率等指標接入 Prometheus/Grafana。
  • 優雅發布:發布新版本時發送 SIGTERM,待進程處理完當前任務退出,systemd 拉起新版本。

常見坑位避雷

  1. PCNTL 僅 CLI 可用:Web 環境(FPM/Apache)禁用 pcntl_fork,必須通過 CLI 入口啟動。
  2. MySQL 連接斷開:fork 後子進程繼承父進程的 DB 連接,需在子進程中重新連接或使用 mysqli::reconnect()。
  3. 文件描述符洩漏:fork 前打開的文件/套接字在子進程中需顯式關閉或重新打開。
  4. 信號處理競態:PHP 5.3+ 建議使用 pcntl_signal_dispatch() 輪詢而非 declare(ticks=1)。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved