第 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 后继承);PIPE_BUF=4096 Linux 原子写保证

      §44.1;pipe 容量 = 65536 字节(2.6.11+);写大于 PIPE_BUF 可能与其他写交错

      pipe() 创建 + fd 5/8 步骤

      int filedes[2]; pipe(filedes); 返回读 (filedes[0])、写 (filedes[1]) 端;fork 后子继承;通常父关 fd[0] 写 + 子关 fd[1] 读

      §44.2;用 dup2() 把 fd[1] 复制到 STDOUT、用 fd[0] 复制到 STDIN 就能构造 shell pipeline

      FIFO / 命名管道

      mkfifo(path, mode) 在文件系统创建一个特殊文件;open 它像打开普通文件;但 read/write 在两个进程间传递数据

      §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;pipe_sync 模式:父建 pipe → fork N 子 → 子 close 读端、做完 close 写端 → 父 read 阻塞 → 全 close 后 EOF

      popen / pclose + 关闭 fd 后阶段

      popen(comm, "r") 返回 FILE*,封装 fork+exec+pipe;pclose() 等待子进程、关闭 fp;可读 output ("r") 或写 input ("w")

      §44.5;"r" 把子进程的 STDOUT 连到管子读端;"w" 把子进程的 STDIN 连到管子写端

      二、详细笔记

      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。

      三、关键图表

      非可视化条目(管道参数与关闭规则)
      项 描述

      pipe(filedes)

      创建无名管道;返回 0=读,1=写

      pipe2(filedes, flags)

      2.6.27+;可加 O_CLOEXEC 或 O_NONBLOCK

      O_NONBLOCK

      read 空管返回 EAGAIN;write 满管返回 EAGAIN

      F_SETPIPE_SZ / F_GETPIPE_SZ

      2.6.35+ 调整管道容量(最大 /proc/sys/fs/pipe-max-size)

      PIPE_BUF

      原子写阈值(Linux=4096);fpathconf(fd, _PC_PIPE_BUF) 查

      关闭未用 fd

      关键:否则 read 不返回 EOF,write 不 SIGPIPE

      mkfifo(path, mode)

      创建命名管道;lstat 标识为 p 模式

      FIFO open 默认阻塞

      必须对端也 open,除非 O_NONBLOCK

      read 返回 0 字节

      当所有写端 close 时返回 EOF

      write 后对端 read 已关

      触发 SIGPIPE(默认杀进程)或 EPIPE

      管道容量 2.6.11+

      65536 字节,可调整

      popen(comm, mode)

      封装 fork+exec+pipe;返回 FILE*

      pclose(fp)

      等子进程终止 + 返回状态

      dup2(fd, 0/1/2)

      shell pipeline 模拟

      signal(SIGPIPE, SIG_IGN)

      写端不想被杀死时

      管 vs FIFO 关键差异
      属性 匿名管道 (pipe) FIFO (命名管道)

      创建

      pipe(fd)

      mkfifo(path, mode)

      标识符

      fd

      pathname

      通信双方

      fork 后相关进程

      任意不相关进程

      文件系统条目

      无

      有(ls 可见)

      open 行为

      已有 fd

      默认阻塞到对端 open

      持久性

      进程级(fd 关就丢)

      名称持久,数据进程级

      pipe 容量

      同 (65536+ Linux)

      同

      四、思维导图

      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

      五、重点与易错点

      1. 「关闭未用 fd」是管道的灵魂规则——漏关一端要么 read 永远阻塞、要么 write 触发 SIGPIPE/EPIPE。

      2. PIPE_BUF 内 write 原子保证——Linux=4096;小消息协议可以省去「消息边界」的设计;超过则可能跨块。

      3. pipe 容量 2.6.11+ 是 65536——比 2.6.11 前的 4096 大;写满 write 阻塞;fcntl(F_SETPIPE_SZ) 可调。

      4. pipe 总是单向——双向通信创建两个 pipe。stream pipes in SysV 是双向但不可移植;用 socketpair(2) 替代。

      5. 管道只能用于相关进程——除非用 UNIX 域套接字传递 fd(§61.13.3)或 FIFO。

      6. FIFO 必须 open 双方都到才不阻塞——典型错误:单独 open(RDWR) 永远不会真正交换数据。

      7. mkfifo 设 umask 前 mode——否则被 umask 屏蔽。

      8. shell pipeline 不会复制大文件——是字节流协议,多线程 sync 得多 pipe。

      9. popen 用 shell 解析 command——小心 command 注入;用单引号包用户输入不够(命令中嵌入单引号困难但可做到)。

      10. pclose 不调 fclose——必须 popen/pclose 配对,fclose 后 pclose 行为未定义。

      11. pipe2(2) 简化的 O_CLOEXEC——2.6.27+;fork+exec 程序用 pipe2 避免 race。

      12. PIPE_BUF 路径——<limits.h> 定义;fpathconf(fd, _PC_PIPE_BUF) 查实际值(可能 > _POSIX_PIPE_BUF)。

      13. O_NONBLOCK 写入大块——返回部分写入(partial write),需循环处理;不像同步阻塞的「全部写入」。

      14. 跨章衔接:第 24 章 fork;第 27 章 exec;第 43 章 IPC 总论;第 46/47/48 章 SysV IPC;第 56-61 章 socket;第 63 章 select/poll/epoll。