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

PHP 多進程管道通訊完整實作與原理解析

     編輯:PHP 相關知識

什麼是管道通訊?

管道是 Unix/Linux 系統中最古老的進程間通訊(IPC)機制之一。可視為一個先進先出(FIFO)的佇列:寫入端將資料放入管道尾端,讀取端從管道頭部取出。管道具有以下特性:

  • 預設大小 4KB(64 位系統通常 64KB),滿時寫入阻塞,空時讀取阻塞。
  • 預設為阻塞模式:寫入後等待讀取,讀取時等待寫入。
  • 可透過 stream_set_blocking() 切換為非阻塞模式。
  • 管道本身是一個特殊檔案(FIFO),存在於檔案系統中(通常在 /tmp)。

PHP 管道類別完整實作

以下封裝建立、讀寫、關閉、刪除管道的完整類別,支援阻塞與非阻塞模式:

<?php
class Pipe {
    private $fifoPath;
    private $wPipe; // 寫入控制代碼
    private $rPipe; // 讀取控制代碼
    
    /**
     * 建立命名管道
     * @param string $name 管道名稱
     * @param int $mode 權限(預設 0666 所有人可讀寫)
     */
    public function __construct($name = 'pipe', $mode = 0666) {
        $this->fifoPath = "/tmp/{$name}" . posix_getpid();
        if (!file_exists($this->fifoPath)) {
            if (!posix_mkfifo($this->fifoPath, $mode)) {
                throw new RuntimeException("建立管道失敗: {$this->fifoPath}");
            }
        } else {
            throw new RuntimeException("管道已存在: {$this->fifoPath}");
        }
    }
    
    // ========== 寫入相關 ==========
    public function openWrite() {
        $this->wPipe = fopen($this->fifoPath, 'w');
        if (!$this->wPipe) throw new RuntimeException("開啟管道寫入失敗");
        return true;
    }
    
    public function write($data) {
        return fwrite($this->wPipe, $data);
    }
    
    public function writeAll($data) {
        $fp = fopen($this->fifoPath, 'w');
        $written = fwrite($fp, $data);
        fclose($fp);
        return $written;
    }
    
    public function closeWrite() {
        return fclose($this->wPipe);
    }
    
    // ========== 讀取相關 ==========
    public function openRead() {
        $this->rPipe = fopen($this->fifoPath, 'r');
        if (!$this->rPipe) throw new RuntimeException("開啟管道讀取失敗");
        return true;
    }
    
    public function read($bytes = 1024) {
        return fread($this->rPipe, $bytes);
    }
    
    public function readAll() {
        $fp = fopen($this->fifoPath, 'r');
        $data = '';
        while (!feof($fp)) {
            $data .= fread($fp, 1024);
        }
        fclose($fp);
        return $data;
    }
    
    public function closeRead() {
        return fclose($this->rPipe);
    }
    
    // ========== 工具方法 ==========
    public function setNonBlocking($handle) {
        return stream_set_blocking($handle, 0);
    }
    
    public function remove() {
        return unlink($this->fifoPath);
    }
    
    public function __destruct() {
        $this->remove();
    }
}

生產者-消費者範例

使用 pcntl_fork() 建立子進程,父進程寫入,子進程讀取:

<?php
require 'Pipe.php';

$pipe = new Pipe('demo_pipe');
$pid = pcntl_fork();

if ($pid == -1) {
    die('Fork 失敗');
} elseif ($pid > 0) {
    // 父進程:生產者
    $pipe->openWrite();
    $pipe->setNonBlocking($pipe->wPipe);
    
    for ($i = 1; $i <= 10; $i++) {
        $msg = "訊息 #$i " . date('H:i:s') . "\n";
        $pipe->write($msg);
        echo "[父] 寫入: $msg";
        sleep(1);
    }
    $pipe->closeWrite();
    pcntl_wait($status); // 等待子進程結束
    echo "[父] 完成\n";
} else {
    // 子進程:消費者
    $pipe->openRead();
    $pipe->setNonBlocking($pipe->rPipe);
    
    $count = 0;
    while ($count < 10) {
        $data = $pipe->read(1024);
        if ($data !== '' && $data !== false) {
            echo "[子] 讀取: $data";
            $count++;
        } else {
            usleep(100000); // 0.1 秒避免 CPU 空轉
        }
    }
    $pipe->closeRead();
    exit(0);
}

關鍵技術細節

  1. posix_mkfifo():建立命名管道(FIFO),需 PHP 編譯支援 --enable-pcntl 且在 Linux/Unix 環境。
  2. 阻塞 vs 非阻塞:預設阻塞模式下,fwrite() 會等待讀取端;非阻塞模式需自行處理 EAGAIN 錯誤。
  3. 訊號處理:生產環境應註冊 SIGTERM、SIGCHLD 訊號處理器,優雅關閉管道並清理資源。
  4. 管道大小限制:單次寫入超過 PIPE_BUF(通常 4096/65536 位元組)可能非原子性,大數據建議分塊寫入。

替代方案比較

方案優點缺點
命名管道 (FIFO)系統級、跨進程、支援檔案權限單向、僅限本機、大小固定
Unix Domain Socket雙向、高效能、支援傳遞檔案描述符程式較複雜
Message Queue (sysvmsg)持久化、優先級、結構化訊息需核心支援、設定較繁
Redis/RabbitMQ分散式、高可用、功能豐富引入外部依賴、延遲較高

適用場景

  • 單機多進程任務分發(如:主進程分派任務給 Worker)
  • 日誌收集器:多進程寫入,單進程彙總寫入磁碟
  • 簡單的生產者-消費者模型,不想引入重型中介軟體

注意:Windows 不支援 posix_mkfifo,若需跨平台請改用 Socket 或佇列服務。

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