PHP 透過 `pcntl` 擴充(需編譯 `--enable-pcntl`、Linux/Unix 環境)可實現真正的多程序平行處理,適用於資料採集、郵件群發、長時間任務、TCP 服務等場景。本文系統介紹核心函式、進程建立與回收、訊號處理、僵屍進程防範、雙重 Fork 技巧,並提供可部署的 `Signfork` 類別與實戰範例。
| 函式 | 用途 |
|---|---|
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 |
<?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` 回收,否則累積導致無法建立新進程。
pcntl_signal(SIGCHLD, function ($signo) {
while (pcntl_waitpid(-1, $status, WNOHANG) > 0) {
// 子進程結束,可記錄日誌
}
});
pcntl_signal(SIGCHLD, SIG_IGN); // 父進程不關心子進程結束,核心自動清理
$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 失敗');
}
// 第二層孫進程:無控制終端機、非會話領導者,無法重新獲得終端機
// 執行長時間任務...
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');