编程语言研究书库

管道与 FIFO — 字节流、dup2 与 shell 管道的实现

管道与 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))

  1. 无边界字节流:写 100 字节,读可以只取 32——字节之间没有 「消息」刻度(要消息看第 12 章消息队列);
  2. 先进先出:读出的顺序与写入一致;
  3. 读走即毁:读过的字节被移除,两个读者绝不会读到同一份数据;
  4. 空管读阻塞(默认):写端全关 → read 返 0(像 EOF); 写端还开着 → 睡到有数据;
  5. 容量有限(Linux 16 页 = 65,536 字节):满管写阻塞, O_NONBLOCK 时返 EAGAIN;
  6. ≤PIPE_BUF(4096) 的写是原子的——多进程同时各写 ≤4096 字节, 不会互相插行;超过就可能交错;
  7. 不能 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. 最佳实践清单(原书汇总)

  1. 写没有读者的管道 → SIGPIPE/EPIPE(捕获信号后 write 才返错);
  2. 只要还有一个写者开着,读就阻塞——写端不关干净,读者永眠;
  3. 满管写阻塞到腾出 PIPE_BUF 空间;
  4. 读是消费性的,多读者各取一份互不重复;
  5. ≤PIPE_BUF 写原子;有数据时发起读立即返回(不会被截断地返回 已有数据);
  6. 管道不能 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, 永远阻塞。

  1. 管道 = 内核里的单向字节流:无边界、FIFO 序、读走即毁、 不能 seek。
  2. 容量 64KB;写 ≤PIPE_BUF(4096) 原子,超过可能交错。
  3. pipe 必须在 fork 前调;fd 成对出现,pipefd[0] 读 [1] 写。
  4. 每个进程必须 close 自己不用的端——读者留写端 = 等不到 EOF 的永久阻塞。
  5. 写无读者的管道 = SIGPIPE(默认杀进程)/捕获后 EPIPE。
  6. dup2(old,new) 原子地把 new 换成 old 的副本——接管道进 stdin/stdout 的唯一安全做法;dup 已废弃。
  7. shell 的竖线 = fork ×2 + dup2(fd,1)/dup2(fd,0) + execlp。
  8. popen("cmd","r"/"w") 一键管道化外部命令;pclose 关并收尸, 不能 fclose。
  9. FIFO = 有名管道:mkfifo 建、open 开、内核对象不占磁盘; 两端齐开才能写,阻塞 open 会等对端。
  10. 公共 FIFO 放 /var/tmp(重启不清理)或 /run/<服务>/。

11. 原文地图

主题原书章原文位置
七条语义与容量/原子写Chapter 13: Pipes and FIFOstext/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」)

Footnotes

  1. 出处:「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」)。 ↩ ↩2

  2. 出处:「Chapter 13: Pipes and FIFOs」第 322–356 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:343,搜「SIGPIPE」) · :804(搜「SIGPIPE signal will be sent」)。 ↩

  3. 出处:「Chapter 13: Pipes and FIFOs」第 136–158 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:141,搜「int pipe」)。 ↩

  4. 出处:「Chapter 13: Pipes and FIFOs」第 245–269 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:251,搜「block permanently」)与第 162–205 段的 pipe_demo1。 ↩

  5. 出处:「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」)。 ↩

  6. 出处:「Chapter 13: Pipes and FIFOs」第 485–601 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:535,搜「pipe:[94990]」) · :558(搜「pipe:[94990]」)。 ↩ ↩2

  7. 出处:「Chapter 13: Pipes and FIFOs」第 603–654 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:618,搜「int dup」) · :623(搜「lowest-numbered」)。 ↩

  8. 出处:「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」)。 ↩

  9. 出处:「Chapter 13: Pipes and FIFOs」第 718–730 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:729,搜「pipefd[0] != 0」)。 ↩

  10. 出处:「Chapter 13: Pipes and FIFOs」第 666–674 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:670,搜「race condition」)。 ↩ ↩2 ↩3

  11. 出处:「Chapter 13: Pipes and FIFOs」第 833–918 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:853,搜「FILE *popen」) · :871(搜「-c」) · :913(搜「pclose」)。 ↩

  12. 出处:「Chapter 13: Pipes and FIFOs」第 920 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:920,搜「pclose」)。 ↩

  13. 出处:「Chapter 13: Pipes and FIFOs」第 977–987 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:982,搜「block buffering」)。 ↩

  14. 出处:「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」)。 ↩ ↩2

  15. 出处:「Chapter 13: Pipes and FIFOs」第 988–1025 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:1004,搜「directory entries」) · :1011(搜「kernel memory」)。 ↩

  16. 出处:「Chapter 13: Pipes and FIFOs」第 1027–1104 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:1100,搜「int mkfifo」) · :1055(搜「prw-r--r--」)。 ↩

  17. 出处:「Chapter 13: Pipes and FIFOs」第 1105–1129 段(text/19-ch13-chapter-13-pipes-and-fifos.txt:1112,搜「well-known FIFOs」)。 ↩ ↩2

  18. 出处:「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」)。 ↩ ↩2 ↩3

  19. 出处:「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」)。 ↩

On this page