第 44 章 管道与 FIFO (Pipes and FIFOs)
核心结论
-
管道 (pipe) 三特性:byte stream(无消息边界)、单向(半双工,stream pipes 在 SysV 是全双工但不可移植)、相关进程间通信(fork 后共享 fd);
PIPE_BUF=4096 (Linux)字节内的 write 原子。 -
pipe() + fork() 模板:父创建 pipe → fork → 父关读端 + 写、子关写端 + 读——单向父子管道。双向通信须两管道。
-
FIFO(命名管道):mkfifo 创建文件系统里的特殊文件;open 阻塞直到另一端也 open;让*无关*进程也能通信;数据通过内核缓冲区(同 pipe)。
-
关闭未用 fd 是关键:读端不关 → read 永不返回 EOF;写端不关 → 对端已经 close 还会 SIGPIPE/EPIPE——必须关。
-
进程同步 + shell 集成:pipe 用作多子进程的同步机制(所有子 close 写端 → 父 read 返回 EOF);shell 用 dup2 把管子和 stdin/stdout 链接,exec 任何 filter 就能拼成 pipeline。
-
popen() / pclose():封装 fork+exec+pipe;popen(comm, "r") 用 fp 读 comm 的 stdout;pclose 等子进程。
|
本章主旨
管道是 UNIX 最老的 IPC——1970 年代就存在,至今仍是大多数进程间数据流的默认选择。本章讲清三大主题:(1) 管道特性(字节流、单向、相关进程、PIPE_BUF 原子保证、close 规则);(2) 管道创建步骤 + shell 集成原理 + 进程同步;(3) FIFO 的应用——mkfifo 创建、不相关进程、open 阻塞模式。读者应能在父子进程、shell pipeline、popen wrapper 中独立使用管子和 FIFO,并理解 close(fd) 的「signal 化」作用。 |
一、核心概念
本章围绕 6 个核心概念展开:从管道特性、创建步骤、FIFO、关闭规则、shell 集成、popen 封装。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
管道三特性 |
byte stream(无消息边界)、单向(半双工)、相关进程(pipe/fork 后继承); |
§44.1;pipe 容量 = 65536 字节(2.6.11+);写大于 PIPE_BUF 可能与其他写交错 |
pipe() 创建 + fd 5/8 步骤 |
|
§44.2;用 |
FIFO / 命名管道 |
|
§44.7;open FIFO 默认阻塞直到对端也 open;非相关进程也能用 |
关闭未用 fd(关键!) |
读进程不关写端 → read 永远阻塞不返回 EOF;写进程不关读端 → write 后对端已 close 会 SIGPIPE |
§44.2;本端 close + 对端 close + 没有数据 = EOF;只关本端不够,必须每进程都关自己不用的一端 |
进程同步 + shell pipeline |
pipe 用作「多个子进程完成时通知父」:所有子 close 写端后,父 read 返回 EOF;shell pipeline = dup2 + fork + exec |
§44.3-4; |
popen / pclose + 关闭 fd 后阶段 |
|
§44.5; |
二、详细笔记
44.1 管道概述
What:pipe 是字节流、单向、半双工、内核缓冲区;只能用于 fork 创建的相关进程。
Why:shell pipeline cmd1 | cmd2 的本质;匿名、最常用。
How——6 个关键特性:
1. 字节流:无消息边界
2. 单向:数据只能一个方向
3. 读阻塞:空管 read 阻塞;write 端关 → EOF
4. PIPE_BUF 原子写保证(Linux=4096)
5. 缓冲区有限:2.6.11 起 65536 字节;满了 write 阻塞
6. 相关进程:fork 继承;socketpair 提供双向变体
管道容量调整(2.6.35+):
// 摘自《The Linux Programming Interface》 第 44 章
#include <fcntl.h>
fcntl(fd, F_SETPIPE_SZ, 1048576); /* 调整容量 */
int sz = fcntl(fd, F_GETPIPE_SZ); /* 查当前容量 */
When:匿名父子 / shell pipeline / 自带 pipe 同步。
Example(simple_pipe.c 一句话):
// 摘自《The Linux Programming Interface》 第 44 章 Listing 44-2
if (pipe(pfd) == -1) errExit("pipe"); /* pfd[0]/pfd[1] */
switch (fork()) {
case -1: errExit("fork");
case 0: /* Child reads */
if (close(pfd[1]) == -1) errExit("close - child");
/* ... read pfd[0] in loop until EOF ... */
break;
default: /* Parent writes */
if (close(pfd[0]) == -1) errExit("close - parent");
write(pfd[1], argv[1], strlen(argv[1]));
close(pfd[1]);
wait(NULL);
}
44.2 pipe() + fork()
What:pipe(filedes) 返回 [0]=读端、[1]=写端;fork 后两进程都持有两端;必须各自关自己不用的一端才能单向工作。
Why:管道是单向;两端都被一个进程持有时无法给对端发 EOF。
How——「父写子读」模板:
// 摘自《The Linux Programming Interface》 第 44 章 Listing 44-1
int filedes[2];
if (pipe(filedes) == -1) errExit("pipe");
switch (fork()) {
case -1: errExit("fork");
case 0: /* Child */
if (close(filedes[1]) == -1) /* 关写端 */
errExit("close");
/* Child reads from pipe */
break;
default: /* Parent */
if (close(filedes[0]) == -1) /* 关读端 */
errExit("close");
/* Parent writes to pipe */
break;
}
重要:双向通信需要 两个 pipe——一个父→子、一个子→父。
When:父子通信、shell pipeline、fork+exec 服务进程。
Example:pipe(2) 失败返回 -1 + errno。
44.3 管道用作进程同步
What:pipe 的「读返回 EOF」可作为「所有子进程完成」的信号。
Why:允许多子进程同步;比 SIGCHLD 更可靠(信号非排队,多个 SIGCHLD 会合并)。
How——pipe_sync.c 模式:
// 摘自《The Linux Programming Interface》 第 44 章 Listing 44-3
int pfd[2];
pipe(pfd); /* 同步管 */
for (j = 1; j < argc; j++) {
switch (fork()) {
case 0: /* 子进程 */
close(pfd[0]); /* 不读 */
sleep(...); /* 模拟工作 */
close(pfd[1]); /* 完成信号 */
_exit(EXIT_SUCCESS);
}
}
close(pfd[1]); /* 父也关写端 */
int dummy;
if (read(pfd[0], &dummy, 1) != 0) /* 阻塞到 EOF */
fatal("parent didn't get EOF");
/* 现在所有子都完成了 */
关键:父也必须 close(pfd[1]),否则 read 永不返回 EOF。
When:需要等待多个子进程完成;典型应用 = worker pool 关闭收尾。
Example:./pipe_sync 4 2 6 → 3 个子进程各 sleep 后 close 写端 → 父顺序收 EOF。
44.4 管道连接过滤器 / shell pipeline
What:shell pipeline ls | wc -l 的实现——用 dup2 把管子两端分别绑到子进程的 STDOUT/STDIN。
Why:shell 用 pipe 把任意 filter 拼成 pipeline;理解 dup2 + 0/1/2 的 fd 语义。
How——pipe_ls_wc.c(§44.4 Listing 44-4):
// 摘自《The Linux Programming Interface》 第 44 章 Listing 44-4
int pfd[2];
pipe(pfd);
switch (fork()) {
case -1: errExit("fork");
case 0: /* 子 1:exec ls 写管子 */
close(pfd[0]);
if (pfd[1] != STDOUT_FILENO) { /* defensive */
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
}
execlp("ls", "ls", (char *) NULL);
default: /* 父再 fork */
break;
}
switch (fork()) {
case 0: /* 子 2:exec wc 读管子 */
close(pfd[1]);
if (pfd[0] != STDIN_FILENO) {
dup2(pfd[0], STDIN_FILENO);
close(pfd[0]);
}
execlp("wc", "wc", "-l", (char *) NULL);
}
/* 父关两端,wait 两子 */
close(pfd[0]); close(pfd[1]);
wait(NULL); wait(NULL);
defensive pfd[1] != STDOUT_FILENO 检查:防止 stdin/stdout 已被 close 时 dup2() 出现 dup2(1, 1) 的 no-op + 错误 close。
When:写一个「构建 pipeline 的库」时;实现自己的 mini-shell。
44.5 popen / pclose
What:popen(command, "r"|"w") 启动 shell 执行 command 并把其 stdout 或 stdin 接到调用进程的 FILE*;pclose() 等子进程终止。
Why:不想 fork+exec+pipe+fdopen 的样板代码时。
How:
// 摘自《The Linux Programming Interface》 第 44 章
#include <stdio.h>
FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);
/* 读 ls 输出 */
FILE *fp = popen("ls -l", "r");
char buf[1024];
while (fgets(buf, sizeof buf, fp) != NULL)
fputs(buf, stdout);
pclose(fp);
/* 写 sort 的 stdin */
FILE *fp = popen("sort -n", "w");
fprintf(fp, "10\n");
fprintf(fp, "1\n");
fclose(fp); /* 也可 pclose */
When:读 shell 命令输出;把数据喂给 shell 管道。
44.7 FIFO(命名管道)
What:mkfifo(pathname, mode) 在文件系统创建特殊文件;进程 open 它像打开文件,但数据在两个进程间传递。
Why:让*无关*进程通信——管道只能在 fork 后使用。
How:
// 摘自《The Linux Programming Interface》 第 44 章
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/* writer */
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "hello", 5);
close(fd);
/* reader */
int fd = open("/tmp/myfifo", O_RDONLY);
read(fd, buf, sizeof buf);
close(fd);
open 阻塞:默认 open() FIFO 直到另一端也 open();多个 writer 可能有竞争——常配合 O_NONBLOCK 或预定义协议(先写 PID 再写数据)。
When:shell 命令之间;服务器与客户端 IPC(替代 Unix socket 或命名管道);日志守护进程。
Example(§44.7 Listing 44-6 fifo_seqnum.c):
一个 server 创建 FIFO + 用 O_WRONLY | O_NONBLOCK open 写 seqnum;多个 client O_RDONLY open 读 seqnum。
三、关键图表
|
非可视化条目(管道参数与关闭规则)
|
|
管 vs FIFO 关键差异
|
四、思维导图
mindmap
root((第 44 章 管道与 FIFO))
管道特性
byte stream
单向
fork 继承
PIPE_BUF 原子
pipe 使用
pipe 返回 fd 0/1
fork 后各自关一端
父子单向通信
关闭规则
关写端 read EOF
关读端 write SIGPIPE
关键 两边都关
同步与 shell
pipe_sync 父等子
dup2 绑 stdin stdout
shell pipeline
popen
fork exec pipe 封装
FILE 返回
r 或 w
FIFO 命名管道
mkfifo 创建
pathname 标识
open 默认阻塞
不相关进程
容量与缓冲
PIPE_BUF 4096
容量 65536
F_SETPIPE_SZ
五、重点与易错点
-
「关闭未用 fd」是管道的灵魂规则——漏关一端要么 read 永远阻塞、要么 write 触发 SIGPIPE/EPIPE。
-
PIPE_BUF 内 write 原子保证——Linux=4096;小消息协议可以省去「消息边界」的设计;超过则可能跨块。
-
pipe 容量 2.6.11+ 是 65536——比 2.6.11 前的 4096 大;写满 write 阻塞;fcntl(F_SETPIPE_SZ) 可调。
-
pipe 总是单向——双向通信创建两个 pipe。stream pipes in SysV 是双向但不可移植;用 socketpair(2) 替代。
-
管道只能用于相关进程——除非用 UNIX 域套接字传递 fd(§61.13.3)或 FIFO。
-
FIFO 必须 open 双方都到才不阻塞——典型错误:单独 open(RDWR) 永远不会真正交换数据。
-
mkfifo 设 umask 前 mode——否则被 umask 屏蔽。
-
shell pipeline 不会复制大文件——是字节流协议,多线程 sync 得多 pipe。
-
popen 用 shell 解析 command——小心 command 注入;用单引号包用户输入不够(命令中嵌入单引号困难但可做到)。
-
pclose 不调 fclose——必须 popen/pclose 配对,fclose 后 pclose 行为未定义。
-
pipe2(2) 简化的 O_CLOEXEC——2.6.27+;fork+exec 程序用 pipe2 避免 race。
-
PIPE_BUF 路径——
<limits.h>定义;fpathconf(fd, _PC_PIPE_BUF)查实际值(可能 >_POSIX_PIPE_BUF)。 -
O_NONBLOCK 写入大块——返回部分写入(partial write),需循环处理;不像同步阻塞的「全部写入」。
-
跨章衔接:第 24 章 fork;第 27 章 exec;第 43 章 IPC 总论;第 46/47/48 章 SysV IPC;第 56-61 章 socket;第 63 章 select/poll/epoll。