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

PHP 多進程編程實戰:pcntl_fork、信號處理、僵屍進程防範與 Signfork 庫封裝

     編輯:PHP 綜合知識

PHP 的 pcntl 擴展(需 --enable-pcntl 編譯)提供類 C 的進程控制能力,適用於 CLI 模式下的數據採集、郵件群發、長連接服務等場景。本文系統講解核心函數、典型模式、僵屍進程處理與生產級封裝。

環境要求

  • Linux/Unix 系統(Windows 不支持 pcntl)
  • PHP 編譯啟用 --enable-pcntl 及 posix 擴展
  • 僅限 CLI SAPI 運行(Web-FPM 模式禁用)

核心函數速查表

函數用途關鍵點
pcntl_fork()創建子進程父進程返回子 PID(>0),子進程返回 0,失敗 -1
pcntl_wait($status)阻塞等待任一子進程結束回收僵屍進程,$status 獲取退出碼
pcntl_waitpid($pid, $status, WNOHANG)非阻塞等待指定/任意子進程WNOHANG 使無結束子進程時立即返回 0
pcntl_signal($signo, $handler)註冊信號處理器需 declare(ticks=1) 啟用 tick 機制
pcntl_alarm($seconds)定時發送 SIGALRM配合信號處理實現定時任務
pcntl_exec($path, $args, $envs)當前進程空間執行新程序類 C exec 族,代碼替換,進程不變
posix_kill($pid, $sig)發送信號給進程SIGTERM 正常終止,SIGKILL 強制殺死
posix_getpid() / getmypid()獲取當前進程 ID
posix_getppid()獲取父進程 ID若返回 1 說明父進程已退出,被 init 收養

基本模式:父進程並行管理多個子進程

 0) {
        $pids[] = $pid; // 父進程記錄子 PID
    } else {
        // 子進程邏輯
        $taskId = $i;
        echo "Child $taskId (PID: " . getmypid() . ") start\n";
        sleep(rand(1, 3)); // 模擬工作
        echo "Child $taskId end\n";
        exit(0); // 關鍵:子進程必須 exit,否則會繼續執行父代碼
    }
}

// 父進程:非阻塞輪詢回收
while (count($pids) > 0) {
    foreach ($pids as $k => $pid) {
        $res = pcntl_waitpid($pid, $status, WNOHANG);
        if ($res === $pid) { // 該子進程已結束
            echo "Reaped child $pid, status: $status\n";
            unset($pids[$k]);
        }
    }
    sleep(1);
}
echo "All done\n";

信號驅動非阻塞回收(推薦生產環境)

declare(ticks = 1);
pcntl_signal(SIGCHLD, function($signo) {
    while (($pid = pcntl_waitpid(-1, $status, WNOHANG)) > 0) {
        echo "SIGCHLD: reaped $pid\n";
    }
});

// 創建子進程...
for ($i=0; $i<3; $i++) {
    if (pcntl_fork() === 0) { /* child work */ exit; }
}

// 父進程主循環
while (true) {
    // do other work
    sleep(5);
}

SIGCHLD 信號在子進程結束時發送給父進程,處理器中循環 WNOHANG 回收所有已結束子進程,避免僵屍堆積。

僵屍進程成因與四種預防策略

  1. 父進程阻塞等待:pcntl_wait() — 簡單但喪失並行性。
  2. 信號處理器回收:pcntl_signal(SIGCHLD, 'handler') — 並行 + 及時回收。
  3. 忽略信號讓內核回收:pcntl_signal(SIGCHLD, SIG_IGN) — 最簡單,內核自動清理,但無法獲取退出狀態。
  4. 雙重 Fork(孤兒進程交給 init):子進程再 fork 孫進程後退出,孫進程被 init (PID 1) 收養,結束由 init 回收。子進程仍需父進程回收。

生產級封裝:Signfork 類

特性:參數分發、臨時文件收集結果、非阻塞輪詢回收、自動清理資源、超過 1000 進程保護。

class Signfork {
    private $tmp_path = '/tmp/';

    public function run($obj, $arg = 1) {
        if (!method_exists($obj, '__fork')) exit("__fork method required");
        $spawns = [];
        if (is_array($arg)) {
            foreach ($arg as $k => $v) {
                $spawns[] = $k;
                $this->spawn($obj, $k, $v);
            }
            $spawns['total'] = count($spawns);
        } elseif (is_int($arg)) {
            for ($i=0; $i<$arg; $i++) $this->spawn($obj, $i);
        } else exit('Bad arg');

        return $this->request($spawns);
    }

    private function spawn($obj, $id, $param = null) {
        if (pcntl_fork() === 0) {
            $cid = getmypid();
            $result = $obj->__fork($param);
            file_put_contents($this->tmp_path . "sfpid_$cid", $result);
            posix_kill($cid, SIGTERM); // 自殺,避免僵屍
            exit;
        }
    }

    private function request($spawns) {
        $data = [];
        $total = isset($spawns['total']) ? $spawns['total'] : count($spawns);
        for ($i=0; $i<$total; $i++) {
            while (!($cid = pcntl_waitpid(-1, $status, WNOHANG))) usleep(30000);
            $file = $this->tmp_path . "sfpid_$cid";
            $key = isset($spawns['total']) ? $spawns[$i] : $i;
            $data[$key] = file_get_contents($file);
            unlink($file);
        }
        return $data;
    }
}

使用示例

class Worker {
    public function __fork($param) {
        sleep(rand(1,3));
        return "Task $param done at " . date('H:i:s');
    }
}

$forker = new Signfork();
$results = $forker->run(new Worker(), ['taskA', 'taskB', 'taskC']);
print_r($results);

Windows 替代方案:curl_multi 並發請求

$urls = [
    'https://localhost/task.php?job=1',
    'https://localhost/task.php?job=2',
    'https://localhost/task.php?job=3',
];
$mh = curl_multi_init();
foreach ($urls as $i => $url) {
    $ch[$i] = curl_init($url);
    curl_setopt($ch[$i], CURLOPT_RETURNTRANSFER, 1);
    curl_multi_add_handle($mh, $ch[$i]);
}
$active = null;
do { curl_multi_exec($mh, $active); } while ($active);
foreach ($urls as $i => $url) {
    echo "Task $i: " . curl_multi_getcontent($ch[$i]) . "\n";
}

守護進程化部署技巧

  • nohup php worker.php > log.txt 2>&1 & — 後台運行,忽略掛起信號。
  • screen -S worker php worker.php — 可重連終端管理。
  • Systemd / Supervisor 管理服務化(推薦生產)。

總結

PHP 多進程核心在於 pcntl_fork + SIGCHLD 信號回收。掌握子進程必須 exit、父進程非阻塞輪詢/信號驅動回收、臨時文件/IPC 傳遞結果三大要素,即可構建高性能並發任務系統。Signfork 類展示了生產級封裝思路,可直接擴展使用。

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