管道与 FIFO — 字节流、dup2 与 shell 管道的实现¶
这一章讲什么: 上一章的共享内存要自己管秩序,管道是相反 的极端:内核把一切都管好,你只管读和写——代价是它只是一根 单向字节流,没有「消息」概念。前半章讲无名管道(pipe)+ fork 的标准配方和一堆「不关端就死锁」的坑;中段用 dup2 亲手实现 shell 的管道运算符,shell 的魔法就此除魅;后半章 讲 FIFO——给管道起个名字,任何进程都能来对接。
1. 顶层全景:一根管子的一生¶
pipe(pipefd) → 内核建两个 OFD(读端 pipefd[0]、写端 pipefd[1])
│ 数据全在内核空间(pipefs 伪文件系统),不在任何进程的地址空间
│ 七条语义:无边界字节流 / FIFO 顺序 / 读走即毁(destructive)
│ 空管读阻塞、写端全关读返 0 / 满管写阻塞(容量 64KB)
│ ≤PIPE_BUF(4096) 的写原子 / 不能 lseek
▼
fork 之后父子各拿一套描述符副本 → 想通信,先「关掉不用的端」
│ 这是头号坑:读者不关写端,EOF 永远不出现,永久阻塞
▼
想让任意程序对接?dup2(pipefd[x], 0 或 1)——把管道端塞进
标准输入/输出 → exec 一个过滤器程序 → shell 的竖线就这么实现
▼
想要有名字的管道?mkfifo 一个路径 → open 两端 → 不相干的
进程也能对接(第 14 章客户端-服务器的地基)
图说:前两步是「造管子和接管子」,中间是「把管子伪装成 stdin/stdout」,最后是「给管子挂牌子」。 主线例子:pipe_demo1 的父子对话——父写一句话进管、子逐字符读出打印;关掉哪个 fd、 什么时候读返回 0,每一步都有状态。
2. 管道的七条语义(pipe(7))¶
- 无边界字节流:写 100 字节,读可以只取 32——字节之间没有 「消息」刻度(要消息看第 12 章消息队列);
- 先进先出:读出的顺序与写入一致;
- 读走即毁:读过的字节被移除,两个读者绝不会读到同一份数据;
- 空管读阻塞(默认):写端全关 → read 返 0(像 EOF); 写端还开着 → 睡到有数据;
- 容量有限(Linux 16 页 = 65,536 字节):满管写阻塞, O_NONBLOCK 时返 EAGAIN;
- ≤PIPE_BUF(4096) 的写是原子的——多进程同时各写 ≤4096 字节, 不会互相插行;超过就可能交错;
- 不能 seek——管道没有「偏移」概念,读就是消费1。
写到没有读者的管道:内核给写者发 SIGPIPE(默认杀死进程); 捕获了则 write 返 -1/EPIPE2。
3. pipe + fork:标准配方与头号坑¶
int pipe(int pipefd[2]) 一次调用拿回一对 fd:pipefd[0]=读、
pipefd[1]=写3。主线例子 pipe_demo1 的四步:
① pipe(pipefd) ← 必须在 fork 之前(孩子才有 fd 副本)
② fork()
③ 孩子:close(pipefd[1]) ← 读者先关写端!(否则见下)
while (read(pipefd[0], &c, 1) > 0) 逐字符打印;
④ 父亲:close(pipefd[0]) ← 写者关读端
write(pipefd[1], argv[1], ...); close(pipefd[1]); ← 关写端=给孩子发 EOF
头号坑:读者如果不关写端,它就永远等不到 EOF——因为「写端 是否还有人在」的判定看的是全系统还有没有人握着写端(包括 它自己);父不写之后不关自己的写端,孩子读到数据尽头时 read 会永久阻塞,而不是返回 04。pipe_demo2(两孩子产消)里 父亲两边都得关——不关写端,消费者在孩子死后仍阻塞; father 还得 wait 收尸5。
还有个 /proc 实证:跑 cat | grep reboot,看
/proc/<pid>/fd/1 是 pipe:[94990]、/proc/<grep pid>/fd/0
是同一个 pipe:[94990]——bash 把 cat 的 stdout 和 grep 的
stdin 接到了同一根管子上,实锤6。
4. dup/dup2:管道接入标准流,shell 除魅¶
想把外部程序(ls、grep——代码改不了)接上管道,得把管道端
塞到它们的 fd 0 或 1 去。dup(oldfd) 复制描述符,总挑
最小的空号;dup2(oldfd, newfd) 原子地「关掉 newfd 并让它
指向 oldfd 所指」7:
孩子 1(跑 cat):dup2(fd[1], 1) ← stdout 指向写端
close(fd[0]); close(fd[1]);
execlp("cat", ...) ← exec 后仍指着管子
孩子 2(跑 grep):dup2(fd[0], 0) ← stdin 指向读端
close(fd[0]); close(fd[1]);
execlp("grep", ...)
父亲:close(fd[0]); close(fd[1]); wait × 2
这就是 shellpipe_demo——shell 管道的全部秘密8。
细节坑:pipe() 也挑最小空号,若 0/1 事先被关,pipefd 可能占
0/1,dup2 自我替换前要先检查 pipefd[0]!=0 && pipefd[1]!=19。
另外 dup() 的「close 再 dup」两步有竞态(信号处理函数可能抢
走刚腾出的号)——所以 dup2 存在,dup 已废弃10。
5. popen/pclose:一键接外部命令¶
popen("命令", "r"/"w") = pipe+fork+exec("/bin/sh","-c",命令)
的合成:模式 r → 返回指向读端的 FILE*(读命令的 stdout);
模式 w → 指向写端(喂命令的 stdin)。pclose 关流并等
命令结束(还能拿它的终止状态——但用别家 wait 收过尸会 ECHILD)11。
警告:popen 的流必须 pclose 关,不能 fclose12。还有
stdio 的坑:管道上的 FILE* 是块缓冲,写进去不一定立刻到对端
——要 fflush 或关缓冲13。popen_demo 用两个 popen 当搬运工
模拟管道:popen_demo 'apropos -s2,3 pipe' 'grep posix'14。
6. FIFO:有名字的管道¶
无名管道只能给「fork 出来的亲戚」用,进程一死就没了。FIFO =
语义完全相同的管道,只是从 open(路径) 打开——文件系统里有
个目录项(p 类型,ls 可见)但没有磁盘内容,纯内核对象15。
创建:mkfifo(路径, mode)(shell 有同名命令;模式过 umask)16。
放哪是设计决策:公共 FIFO(类比网络 well-known 端口)放
/tmp、/var/tmp(重启不清理)或 /run/<服务名>/;私有 FIFO
名字只告诉特定的对方17。
打开语义是 FIFO 独有的坑(POSIX 强制):
- 两端都打开之前不能写数据;
- 阻塞模式下 open 一端会等到另一端也 open(
cat < /tmp/MYFIFO单开读端会卡住,直到有人来写); - 非阻塞:只开读端立即成功,只开写端返 ENXIO;
- O_RDWR 自开两端 Linux 支持但 POSIX 不许、别依赖18。
判断(我们的,不是书里的): FIFO 的「打开握手」语义说明一个 普适设计原则:命名资源要有「就绪信号」。管道靠 close 传播 EOF,FIFO 靠 open 互相等待——都是「生命周期事件当同步用」。 自己设计协议时,HeartBeat/EOF/握手报文扮演的正是这个角色; 缺了它,两端各自活在当下,谁也意识不到对方没了。 如果错,会错在: 有些场景(只发不收的日志管道)其实不需要 双向感知,强行握手反而添堵——判据是「对方消失我要多久知道」。
7. 最佳实践清单(原书汇总)¶
- 写没有读者的管道 → SIGPIPE/EPIPE(捕获信号后 write 才返错);
- 只要还有一个写者开着,读就阻塞——写端不关干净,读者永眠;
- 满管写阻塞到腾出 PIPE_BUF 空间;
- 读是消费性的,多读者各取一份互不重复;
- ≤PIPE_BUF 写原子;有数据时发起读立即返回(不会被截断地返回 已有数据);
- 管道不能 seek19。
8. 作者的判断与证据¶
有明证的:
- 七条语义全部出自 pipe(7)/fifo(7),容量 65,536、PIPE_BUF 4096 是当时内核的具体值1。
- shell 管道的 /proc 实证(cat 的 fd/1 与 grep 的 fd/0 指向同一 pipe inode 94990)是可复现的实验6。
- dup 的竞态与 dup2 的原子性,出自实现层面的分析10。
- FIFO 打开语义表来自 POSIX/fifo(7)18。
作者的判断:
- 「dup 已废弃,用 dup2」——POSIX 时代的明确立场10。
- FIFO 用 O_RDWR 自开两端「别依赖,不可移植」——保守的可移植性 建议18。
- 放 FIFO 的位置(/tmp vs /var/tmp vs /run)给了带安全考量的 决策树,并指向 systemd 的临时目录文档——超出教材的实用17。
- 「popen 有 system 一样的安全忌讳」一笔带过,是负责任的不展开14。
9. 边界与局限¶
- 全双工要两根管道——本章全是单向;双向通信的对偶管道设计 留给第 14 章的客户端-服务器;
- 管道上没有「消息」——要边界得自己加协议(长度前缀/分隔符) 或换消息队列;
- O_NONBLOCK 模式下的读写组合矩阵只给了表,没逐个演示;
- socket 再次出现又略过——本地 socket 是管道的现代替代品 (unix(7)),本书不收;
- fcntl 记录锁、pipe2()(原子设 O_NONBLOCK/close-on-exec) 未展开。
10. 可带走的¶
主线例子,一行复述: pipe→fork→孩子关写端逐字符读、父亲 关读端写完后关写端(EOF)→孩子读到 0 退出;少关一个 fd, 永远阻塞。
- 管道 = 内核里的单向字节流:无边界、FIFO 序、读走即毁、 不能 seek。
- 容量 64KB;写 ≤PIPE_BUF(4096) 原子,超过可能交错。
- pipe 必须在 fork 前调;fd 成对出现,pipefd[0] 读 [1] 写。
- 每个进程必须 close 自己不用的端——读者留写端 = 等不到 EOF 的永久阻塞。
- 写无读者的管道 = SIGPIPE(默认杀进程)/捕获后 EPIPE。
- dup2(old,new) 原子地把 new 换成 old 的副本——接管道进 stdin/stdout 的唯一安全做法;dup 已废弃。
- shell 的竖线 = fork ×2 + dup2(fd,1)/dup2(fd,0) + execlp。
- popen("cmd","r"/"w") 一键管道化外部命令;pclose 关并收尸, 不能 fclose。
- FIFO = 有名管道:mkfifo 建、open 开、内核对象不占磁盘; 两端齐开才能写,阻塞 open 会等对端。
- 公共 FIFO 放 /var/tmp(重启不清理)或 /run/<服务>/。
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 七条语义与容量/原子写 | Chapter 13: Pipes and FIFOs | text/19-ch13-chapter-13-pipes-and-fifos.txt:88(搜「two file descriptors」) · :118(搜「65,536」) · :119(搜「PIPE_BUF」) |
| pipe 与 fork 配方、关端警告 | 同上 | :141(搜「int pipe」) · :251(搜「block permanently」) |
| pipe_demo2 与父关两端 | 同上 | :376(搜「would be held open by the parent」) · :413(搜「Close both ends」) |
| /proc 管道实证 | 同上 | :535(搜「pipe:[94990]」) · :558(搜「pipe:[94990]」) |
| dup/dup2 与竞态 | 同上 | :618(搜「int dup」) · :670(搜「race condition」) |
| shellpipe_demo | 同上 | :678(搜「dup2(pipefd[1], 1)」) · :769(搜「execlp」) |
| pipefd 占 0/1 的坑 | 同上 | :729(搜「pipefd[0] != 0」) |
| popen/pclose | 同上 | :853(搜「FILE *popen」) · :920(搜「pclose」) · :982(搜「block buffering」) |
| FIFO 性质与 mkfifo | 同上 | :1004(搜「directory entries」) · :1100(搜「int mkfifo」) · :1055(搜「prw-r--r--」) |
| 公共/私有 FIFO 位置 | 同上 | :1112(搜「well-known FIFOs」) |
| FIFO 打开语义 | 同上 | :1139(搜「both ends」) · :1142(搜「block until the other end」) |
| 最佳实践汇总 | 同上 | :804(搜「SIGPIPE signal will be sent」) · :831(搜「seek」) |
-
出处:「Chapter 13: Pipes and FIFOs」第 64–126 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:88,搜「two file descriptors」) · :97(搜「preserve the order」) · :118(搜「65,536」) · :119(搜「PIPE_BUF」)。 ↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 322–356 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:343,搜「SIGPIPE」) · :804(搜「SIGPIPE signal will be sent」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 136–158 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:141,搜「int pipe」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 245–269 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:251,搜「block permanently」)与第 162–205 段的 pipe_demo1。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 360–483 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:376,搜「would be held open by the parent」) · :413(搜「Close both ends」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 485–601 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:535,搜「pipe:[94990]」) · :558(搜「pipe:[94990]」)。 ↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 603–654 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:618,搜「int dup」) · :623(搜「lowest-numbered」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 655–796 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:678,搜「dup2(pipefd[1], 1)」)与 shellpipe_demo 程序(:769 搜「execlp」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 718–730 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:729,搜「pipefd[0] != 0」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 666–674 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:670,搜「race condition」)。 ↩↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 833–918 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:853,搜「FILE *popen」) · :871(搜「-c」) · :913(搜「pclose」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 920 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:920,搜「pclose」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 977–987 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:982,搜「block buffering」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 921–976 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:941,搜「popen(argv[1]」) · :972(搜「apropos -s2,3 pipe」)。 ↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 988–1025 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:1004,搜「directory entries」) · :1011(搜「kernel memory」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 1027–1104 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:1100,搜「int mkfifo」) · :1055(搜「prw-r--r--」)。 ↩ -
出处:「Chapter 13: Pipes and FIFOs」第 1105–1129 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:1112,搜「well-known FIFOs」)。 ↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 1131–1153 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:1139,搜「both ends」) · :1142(搜「block until the other end」)。 ↩↩↩ -
出处:「Chapter 13: Pipes and FIFOs」第 798–831 段(
text/19-ch13-chapter-13-pipes-and-fifos.txt:804,搜「SIGPIPE signal will be sent」) · :819(搜「PIPE_BUF」) · :824(搜「atomic」) · :831(搜「seek」)。 ↩