管道 是 Unix/Linux 進程間通訊 (IPC) 最經典的機制。「一切皆檔案」哲學下,管道以檔案描述符抽象資料流,實現「生產者-消費者」模型。本文深入剖析 pipe() 系統呼叫、無名管道與命名管道 (FIFO) 的原理、API、同步特性與完整 C 範例。
| 特性 | 無名管道 | 命名管道 |
|---|---|---|
| 建立方式 | pipe(int fd[2]) 系統呼叫 | mkfifo() / mknod() / Shell mkfifo 指令 |
| 存在形式 | 僅在記憶體 (核心緩衝區) | 檔案系統中有 inode (路徑可見) |
| 通訊雙方 | 親緣進程 (父子、兄弟) | 任意無關進程 (知路徑即可) |
| 生命週期 | 隨進程結束銷毀 | 檔案刪除前持久存在 |
| 方向性 | 單向 (fd[0]讀, fd[1]寫) | 單向 (開啟模式決定) |
#include <unistd.h>
int pipe(int fd[2]);
fd 為長度 2 的整數陣列,函數成功返回時:fd[0] = 讀端檔案描述符fd[1] = 寫端檔案描述符errno)。EMFILE (進程檔案描述符用盡)、ENFILE (系統檔案表滿)、EFAULT (指標無效)。管道容量:Linux 管道預設 64 KB (可用
fcntl(fd, F_SETPIPE_SZ, size)調整,最大/proc/sys/fs/pipe-max-size,預設 1 MB)。寫入滿時阻塞,讀取空時阻塞 —— 內建同步機制。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define MSG "Hello from parent via pipe!"
int main() {
int fd[2];
pid_t pid;
char buf[128];
// 1. 建立管道 (必須在 fork 前)
if (pipe(fd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 2. 建立子進程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) { // ===== 子進程:讀取 =====
close(fd[1]); // 關閉寫端
ssize_t n = read(fd[0], buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\0';
printf("[Child] Received: %s\n", buf);
}
close(fd[0]);
exit(EXIT_SUCCESS);
} else { // ===== 父進程:寫入 =====
close(fd[0]); // 關閉讀端
write(fd[1], MSG, strlen(MSG));
close(fd[1]); // 關閉寫端 → 子進程 read 返回 0 (EOF)
wait(NULL); // 回收子進程
printf("[Parent] Done.\n");
}
return 0;
}
read 永久阻塞。mkfifo /tmp/my_fifo
ls -l /tmp/my_fifo # prw-r--r-- p 表示 FIFO
#include <sys/stat.h>
#include <fcntl.h>
mkfifo("/tmp/my_fifo", 0666); // 返回 0 成功,-1 失敗 (EEXIST 已存在)
open(path, O_RDONLY):阻塞直到有寫端開啟。open(path, O_WRONLY):阻塞直到有讀端開啟。open(path, O_RDONLY | O_NONBLOCK):非阻塞,無寫端時返回 -1 (ENXIO)。writer.c
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd = open("/tmp/my_fifo", O_WRONLY);
if (fd == -1) { perror("open"); return 1; }
write(fd, "Hello FIFO!\n", 12);
close(fd);
return 0;
}
reader.c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("/tmp/my_fifo", O_RDONLY);
if (fd == -1) { perror("open"); return 1; }
char buf[64];
ssize_t n = read(fd, buf, sizeof(buf)-1);
if (n > 0) { buf[n] = '\0'; printf("Received: %s", buf); }
close(fd);
return 0;
}
編譯:gcc writer.c -o writer && gcc reader.c -o reader
執行:終端 1 執行 ./reader (阻塞等待) → 終端 2 執行 ./writer → reader 印出訊息並結束。
POSIX 規定:寫入 ≤ PIPE_BUF (Linux 4096 bytes) 的資料是原子的,不會與其他寫入交織。大於此值可能分段寫入。
|Shell cmd1 | cmd2 內部即透過 pipe() + fork() + dup2() 實現:將 cmd1 標準輸出重導向至管道寫端,cmd2 標準輸入重導向至管道讀端。
| 現象 | 原因 | 修正 |
|---|---|---|
| read 永久阻塞 | 寫端未關閉 (含父進程未關閉 fd[1]) | 所有寫端進程必須關閉寫端 |
| SIGPIPE 導致進程終止 | 讀端已關閉,仍嘗試寫入 | 捕捉/忽略 SIGPIPE,或檢查 write 返回 -1 && errno==EPIPE |
| 命名管道 open 卡住 | 對端未開啟 (預設阻塞模式) | 先開讀端再開寫端,或用 O_NONBLOCK |
| 資料亂序/交織 | 多進程並發寫入超過 PIPE_BUF | 自行加鎖,或改用 Socket/訊息佇列 |
選型建議:親緣進程單向流 →
pipe();無關進程/持久化/多對多 → 命名管道 / Unix Domain Socket / System V 訊息佇列 / POSIX 訊息佇列。