第 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 同步。

      Examplesimple_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()

      Whatpipe(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 服务进程。

      Examplepipe(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

      Whatpopen(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(命名管道)

      Whatmkfifo(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。