PHP 透過 pcntl 擴展支援進程控制,可實作多進程常駐服務(Daemon)。本文提供一個完整的 **TCP 代理伺服器** 範例,展示 pcntl_fork、訊號處理、非阻塞 Socket、子進程管理等核心技術。
pcntl)--enable-pcntl、--enable-sockets以下為精簡重點的架構說明,完整可執行代碼請參考原文。
<?php
// 父進程訊號處理
function parentSignalHandler($signo) {
global $childPid;
switch ($signo) {
case SIGTERM:
case SIGINT:
if ($childPid) {
posix_kill($childPid, SIGTERM); // 通知子進程結束
pcntl_wait($status); // 回收子進程,避免殭屍進程
}
closeSockets();
exit(0);
case SIGCHLD:
// 子進程結束時自動觸發
$childPid = null;
pcntl_wait($status);
break;
}
}
// 子進程訊號處理
function childSignalHandler($signo) {
if (in_array($signo, [SIGTERM, SIGINT])) {
closeSockets();
exit(0);
}
}
?>
<?php
// 第一次 fork:父進程退出,子進程成為孤兒進程,由 init 收養
$pid = pcntl_fork();
if ($pid === -1) die('fork 失敗');
if ($pid > 0) exit(0); // 父進程退出
// 脫離終端機,成為 session leader
if (posix_setsid() === -1) die('無法脫離終端機');
// 第二次 fork(可選):確保無法重新獲得控制終端
$pid = pcntl_fork();
if ($pid === -1) die('fork 失敗');
if ($pid > 0) exit(0);
// 現在是完整的 Daemon 進程
?>
<?php
function processClient($clientSocket) {
global $remoteHost, $remotePort;
declare(ticks = 1); // 必須:啟用訊號處理
pcntl_signal(SIGTERM, 'childSignalHandler');
pcntl_signal(SIGINT, 'childSignalHandler');
// 連線至上游伺服器
$remoteSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($remoteSocket, $remoteHost, $remotePort);
// 設為非阻塞
socket_set_nonblock($clientSocket);
socket_set_nonblock($remoteSocket);
while (true) {
$clientData = @socket_read($clientSocket, 4096, PHP_BINARY_READ);
$remoteData = @socket_read($remoteSocket, 4096, PHP_BINARY_READ);
// 處理連線關閉
if ($clientData === '' || $remoteData === '') break;
// 非阻塞讀取無資料時回傳 false
$clientReady = $clientData !== false;
$remoteReady = $remoteData !== false;
if (!$clientReady && !$remoteReady) {
usleep(100000); // 0.1 秒降低 CPU 佔用
continue;
}
// 雙向轉發資料
if ($clientReady) {
writeAll($remoteSocket, $clientData);
}
if ($remoteReady) {
writeAll($clientSocket, $remoteData);
}
}
closeSockets();
exit(0);
}
function writeAll($socket, $data) {
$written = 0;
$len = strlen($data);
while ($written < $len) {
$n = @socket_write($socket, substr($data, $written), $len - $written);
if ($n === false) {
usleep(100000);
continue;
}
if ($n === 0) break; // 連線中斷
$written += $n;
}
}
?>
<?php
$listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($listenSocket, '0.0.0.0', 9999);
socket_listen($listenSocket);
socket_set_nonblock($listenSocket);
pcntl_signal(SIGTERM, 'parentSignalHandler');
pcntl_signal(SIGINT, 'parentSignalHandler');
pcntl_signal(SIGCHLD, 'parentSignalHandler');
$childPid = null;
while (true) {
sleep(1); // 簡單輪詢,生產環境建議用 socket_select/poll/epoll
$clientSocket = @socket_accept($listenSocket);
if ($clientSocket !== false) {
$pid = pcntl_fork();
if ($pid === -1) die('fork 失敗');
if ($pid > 0) {
// 父進程:關閉客戶端 socket,繼續監聽
socket_close($clientSocket);
$childPid = $pid;
continue;
}
// 子進程:處理連線
processClient($clientSocket);
}
}
?>
| 技術 | 關鍵點 |
|---|---|
pcntl_fork | 複製進程空間,父進程回傳子 PID,子進程回傳 0 |
declare(ticks=1) | 啟用 PHP 訊號處理機制(註冊 pcntl_signal 後必須) |
| 非阻塞 Socket | socket_set_nonblock + 輪詢/事件驅動,避免單進程阻塞 |
| 殭屍進程防止 | 父進程 SIGCHLD 處理器中呼叫 pcntl_wait() |
| 優雅關閉 | 捕捉 SIGTERM/SIGINT,通知子進程、回收資源、關閉 Socket |
| 資料完整轉發 | socket_write 可能部分寫入,需迴圈確保全數發送 |
純 PHP 多進程開發複雜度高、易出錯,生產環境建議考慮:
本範例適合學習底層原理、維護舊系統、或極簡環境無法安裝擴展時使用。新專案強烈建議採用上述成熟框架。