PHP 的 pcntl 擴展(需 --enable-pcntl 編譯)提供類 C 的進程控制能力,適用於 CLI 模式下的數據採集、郵件群發、長連接服務等場景。本文系統講解核心函數、典型模式、僵屍進程處理與生產級封裝。
--enable-pcntl 及 posix 擴展| 函數 | 用途 | 關鍵點 |
|---|---|---|
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 回收所有已結束子進程,避免僵屍堆積。
pcntl_wait() — 簡單但喪失並行性。pcntl_signal(SIGCHLD, 'handler') — 並行 + 及時回收。pcntl_signal(SIGCHLD, SIG_IGN) — 最簡單,內核自動清理,但無法獲取退出狀態。特性:參數分發、臨時文件收集結果、非阻塞輪詢回收、自動清理資源、超過 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);
$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 — 可重連終端管理。PHP 多進程核心在於 pcntl_fork + SIGCHLD 信號回收。掌握子進程必須 exit、父進程非阻塞輪詢/信號驅動回收、臨時文件/IPC 傳遞結果三大要素,即可構建高性能並發任務系統。Signfork 類展示了生產級封裝思路,可直接擴展使用。