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

PHP 多程序編程實戰:pcntl_fork 進程控制、訊號處理、僵屍進程防範與實作範例

     編輯:PHP 相關知識

前言

PHP 透過 `pcntl` 擴充(需編譯 `--enable-pcntl`、Linux/Unix 環境)可實現真正的多程序平行處理,適用於資料採集、郵件群發、長時間任務、TCP 服務等場景。本文系統介紹核心函式、進程建立與回收、訊號處理、僵屍進程防範、雙重 Fork 技巧,並提供可部署的 `Signfork` 類別與實戰範例。

環境需求

  • Linux/Unix(Windows 不支援 `pcntl_fork`)
  • PHP 編譯參數:`--enable-pcntl` + `--enable-posix`
  • CLI 模式執行(Web 模式下 `pcntl_fork` 受限、易導致連線中斷)

核心函式速查

函式用途
pcntl_fork()建立子進程,父進程回傳子 PID,子進程回傳 0
pcntl_wait($status)阻塞等待任一子進程結束,回收資源
pcntl_waitpid($pid, $status, $options)等待指定 PID 子進程,`WNOHANG` 非阻塞
pcntl_signal($signo, $handler)註冊訊號處理器(需 `declare(ticks=1)`)
pcntl_alarm($seconds)設定鬧鐘訊號 `SIGALRM`
pcntl_exec($path, $args, $envs)在當前進程空間執行外部程式(替換當前進程)
posix_kill($pid, $signal)向進程發送訊號
posix_getpid()取得目前進程 PID
posix_getppid()取得父進程 PID

基本 Fork 模板

<?php
declare(ticks = 1); // 訊號處理需要

$pid = pcntl_fork();
if ($pid === -1) {
    die('fork 失敗');
} elseif ($pid > 0) {
    // 父進程
    echo "父進程 PID: " . posix_getpid() . ", 子進程 PID: $pid\n";
    pcntl_wait($status); // 阻塞回收
    echo "子進程結束,狀態: $status\n";
} else {
    // 子進程
    echo "子進程 PID: " . posix_getpid() . " 開始工作\n";
    sleep(3); // 模擬任務
    echo "子進程結束\n";
    exit(0); // 重要:子進程必須 exit,否則會繼續執行父進程後續代碼
}

訊號處理範例:定時器

declare(ticks = 1);

function sigHandler($signo) {
    echo "收到訊號 $signo\n";
    if ($signo === SIGALRM) {
        pcntl_alarm(5); // 重設 5 秒
    }
}
pcntl_signal(SIGALRM, 'sigHandler');
pcntl_alarm(5);

while (true) {
    // 主迴圈做事
    sleep(1);
}

僵屍進程成因與防範

子進程結束後進入「僵屍」狀態,佔用 PID 與進程表項。父進程必須呼叫 `pcntl_wait`/`waitpid` 回收,否則累積導致無法建立新進程。

四大防範策略

  1. 父進程阻塞等待:`pcntl_wait($status)` —— 簡單但父進程無法並行處理。
  2. 非阻塞輪詢:`pcntl_waitpid(-1, $status, WNOHANG)` 在主迴圈定期呼叫。
  3. 訊號處理器回收:
    pcntl_signal(SIGCHLD, function ($signo) {
        while (pcntl_waitpid(-1, $status, WNOHANG) > 0) {
            // 子進程結束,可記錄日誌
        }
    });
  4. 忽略 SIGCHLD 讓核心自動回收(Linux 核心 2.6+):
    pcntl_signal(SIGCHLD, SIG_IGN); // 父進程不關心子進程結束,核心自動清理

雙重 Fork 技巧:讓子進程脫離父進程控制(Daemon 化)

$pid = pcntl_fork();
if ($pid > 0) {
    exit(0); // 父進程退出
} elseif ($pid === -1) {
    die('fork 失敗');
}
// 第一層子進程(現為孤兒,被 init 收養)
posix_setsid(); // 建立新會話,脫離終端機

$pid2 = pcntl_fork();
if ($pid2 > 0) {
    exit(0); // 第一層子進程退出
} elseif ($pid2 === -1) {
    die('fork 失敗');
}
// 第二層孫進程:無控制終端機、非會話領導者,無法重新獲得終端機
// 執行長時間任務...

實戰封裝:Signfork 多任務並行類別

class Signfork {
    private $tmpDir = '/tmp/';
    
    public function run($worker, $tasks) {
        // $tasks: 陣列 [taskId => 參數] 或 整數 N 表示建立 N 個相同任務
        $pids = [];
        $taskList = is_array($tasks) ? $tasks : array_fill(0, $tasks, null);
        
        foreach ($taskList as $id => $param) {
            $pid = pcntl_fork();
            if ($pid === -1) { die("fork 失敗 at $id"); }
            if ($pid > 0) {
                $pids[$pid] = $id; // 父進程記錄 PID 對應任務 ID
                continue;
            }
            // 子進程
            $result = $worker($param, $id);
            $this->saveResult($result);
            posix_kill(posix_getpid(), SIGTERM); // 自殺
            exit(0);
        }
        
        // 父進程收集結果
        $results = [];
        foreach ($pids as $pid => $id) {
            pcntl_waitpid($pid, $status);
            $results[$id] = $this->loadResult($pid);
        }
        return $results;
    }
    
    private function saveResult($data) {
        $file = $this->tmpDir . 'signfork_' . posix_getpid() . '.ser';
        file_put_contents($file, serialize($data));
    }
    private function loadResult($pid) {
        $file = $this->tmpDir . 'signfork_' . $pid . '.ser';
        $data = @unserialize(file_get_contents($file));
        @unlink($file);
        return $data;
    }
}

// 使用範例
$fork = new Signfork();
$results = $fork->run(function($param, $id) {
    sleep(rand(1,3));
    return "Task $id done with param: $param";
}, ['A', 'B', 'C', 'D']);

print_r($results);

背景執行與脫離終端機

// nohup 脫離終端機
system('nohup php worker.php arg1 arg2 > /dev/null 2>&1 &');

// 或使用 screen
system('screen -dmS worker php worker.php arg1 arg2');

總結與最佳實踐

  1. **僅在 CLI 長期運行腳本使用 `pcntl_fork`**,Web 請求中使用極易導致連線洩漏、資源耗盡。
  2. **子進程務必 `exit(0)`**,否則會繼續執行父進程邏輯造成災難。
  3. **僵屍進程防範**:採用 `SIG_IGN` 或訊號處理器自動回收,最簡單可靠。
  4. **進程間通訊 (IPC)**:檔案(如範例)、Unix Socket、Message Queue、Shared Memory、Redis。避免共享記憶體(需 `shmop` 擴充)。
  5. **現代替代方案**:**Swoole / OpenSwoole / RoadRunner** 提供協程、多進程管理、任務調度,效能與開發體驗遠超手寫 `pcntl`。生產環境強烈建議遷移至 Swoole 生態。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved