尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Linux匿名管道全解析:从shell的“|”到进程间通信内核机制

发布时间:2026/9/29 17:02:39

资讯中心
01
ARTICLE

Linux匿名管道全解析:从shell的“|”到进程间通信内核机制

Linux匿名管道全解析:从shell的“|”到进程间通信内核机制
去年处理一个日志分析脚本时我发现一个诡异现象shell 里用grep ... | awk ... | sort ...跑出来的结果经常缺行偶尔还会卡死。那时候我还没把“管道”当回事以为只是脚本写得烂。直到后来用 strace 跟踪系统调用才看清楚问题的真正面目——每一个看上去平平无奇的竖线|背后其实隐藏着一整套最基础也最重要的进程间通信IPC机制匿名管道。这篇文章就围绕匿名管道来讲。它是 Linux 进程间通信里最基础的一种方式是 shell 管道符的底层实现也是面试官最爱从浅处往深处追问的知识点。无论你是刚开始学 Linux 的读者、写系统程序的 C 语言开发者还是搞嵌入式 Linux 经常要处理多进程协作的朋友我都建议把管道这块彻底吃透。我会从 shell 的|讲起一直讲到内核缓冲区、阻塞规则、踩坑实录和面试考点尽量让每个“为什么”都有清晰的答案。1. 你早就用过匿名管道从“|”说起1.1 shell管道符的运行机制我们天天敲ps aux | grep nginx、cat xx.log | tail -n 20但很少想过中间这个竖线到底发生了什么。其实 shell 处理管道符的步骤非常清晰shell 先调用pipe()创建一个匿名管道得到一个读端 fd 和一个写端 fdfork 出两个子进程一个执行左边的命令一个执行右边的命令左边进程把标准输出 stdout 重定向到管道写端dup2右边进程把标准输入 stdin 重定向到管道读端dup2两边各自 exec 真正的程序shell 等待两个进程结束这里有个非常关键的点管道本身并不能“选择发给谁”它只是把数据从写端灌进内核的一个缓冲区读端再从缓冲区取出来。至于数据格式、消息边界这些都是使用者自己规定的。这就像两个人打电话电话线只负责传输语音你们说什么语言、聊什么话题电话线不关心。之前我在给嵌入式设备写一个日志采集程序时就是靠理解这五步把 shell 的重定向逻辑搬到了 C 代码里才彻底解决了子进程输出无法正确收集的问题。1.2 用strace看透管道符我觉得最直观的理解方式是直接用 strace 看 shell 到底调用了哪些系统调用strace -f -e tracepipe,dup2,read,write,close,execve bash -c echo hello | wc -c在这条输出里你会看到一条清晰的链路pipe([3, 4])创建了一个管道读端 fd3写端 fd4先 fork 出子进程再dup2(4, 1)把标准输出指向管道写端另一个子进程dup2(3, 0)把标准输入指向管道读端然后各自execve执行/usr/bin/echo和/usr/bin/wc注意 fd 编号从 3 开始是因为 0、1、2 分别是标准输入、标准输出、标准错误被 bash 自己占着。dup2 的语义就是把旧 fd 复制到新 fd 上——如果新 fd 已经打开先关闭它再复制。这个操作在管道编程里极其重要下面实操会反复用到。为什么我要花这个篇幅讲 shell 管道符因为很多教程直接摆 C 代码读者看完了可能还是没把“命令行的 |”和“C 里的 pipe()”联系起来。一旦你用 strace 看明白 shell 的管道符就是匿名管道的一种典型使用场景后面所有代码都能对号入座。2. C语言实操从零手写一个管道通信2.1 先建管道再fork顺序不能反pipe()是一个系统调用#include unistd.h int pipe(int pipefd[2]);传入一个长度为 2 的数组成功返回 0pipefd[0]是可读端pipefd[1]是可写端失败返回 -1 并设置 errno。记住一个铁律pipe 之后才 fork。为什么因为 fork 会复制父进程的地址空间和文件描述符表父子进程会共享同一组 fd 指向的管道对象。只有先创建管道fork 才能让两边都拿到这组 fd。如果 fork 之后再 pipe那管道只在自己进程里父子之间没有共享通道根本没法通信。这一点在面试里常以“管道创建后 fork 的次序影响”形式出现其实本质上就是问 fd 的继承关系。2.2 关闭多余的文件描述符管道创建后进程里同时握有读端和写端两个 fd。如果父进程想写、子进程想读那么父进程要主动关闭读端子进程要主动关闭写端。这里我强调一下为什么管道有一个特性只有当管道的所有写端都被关闭读端 read 才会返回 0EOF只要还有任何一个写端开着即使没数据可读read 也会一直阻塞等待。反过来也一样当所有读端都关闭了写端再 write 会触发 SIGPIPE 信号进程默认直接退出。所以关闭不用的 fd 不是洁癖是逻辑正确性的一部分。很多初学管道的人父子进程都不关多余端结果发现 read 永远阻塞、write 永远不会返回排查半天找不到原因。2.3 第一个完整demo直接看代码#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/wait.h int main(void) { int pipefd[2]; pid_t pid; char buf[256]; if (pipe(pipefd) -1) { perror(pipe); exit(EXIT_FAILURE); } pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程读端 close(pipefd[1]); // 子进程不写关掉写端 ssize_t n read(pipefd[0], buf, sizeof(buf) - 1); if (n 0) { perror(read); exit(EXIT_FAILURE); } buf[n] \0; printf(child received: %s\n, buf); close(pipefd[0]); exit(EXIT_SUCCESS); } // 父进程写端 close(pipefd[0]); // 父进程不读关掉读端 char *msg hello from parent via pipe; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); // 写完后关写端子进程read才能返回0 wait(NULL); // 回收子进程 return 0; }编译运行gcc pipe_demo.c -o pipe_demo ./pipe_demo看到child received: hello from parent via pipe。整个过程就是父进程往管道里扔了一段字符串子进程从管道里取出来单向流动。代码里每个 close 都有讲究子进程close(pipefd[1])子进程只负责读必须让管道的写端引用数降下来否则 read 永远等不到 EOF父进程close(pipefd[0])父进程只负责写如果不关闭读端多余的读端 fd 会一直存在影响管道引用计数父进程写完再close(pipefd[1])让子进程模拟“对方写完关闭”的场景read 就能正常返回 0如果你的数据不止一行可以把 read 放到 while 循环里直到 n 0 才结束这就是 shell 后台真正在做的事。2.4 半双工限制匿名管道是半双工的数据只能向一个方向流动。如果需要父子进程互发数据那就得创建两个管道一个从父到子一个从子到父。这个细节很多人会忽略用同一个管道来回写结果数据混乱甚至死锁我在踩坑部分会说。3. 匿名管道的底层运行机制3.1 管道在内核里的形态很多资料告诉你管道就是“内存中的一块缓冲区”但落到内核层这个说法其实过于含糊。Linux 中每个管道对应一个pipe_inode_info结构它维护着一个环形缓冲区由一页一页的内存页组成。read 和 write 会更新读写指针读走的数据不会回放这就是“流”的语义。这个缓冲区的大小在 Linux 上默认是 65536 字节64KB由 16 页组成每页 4KB。早期内核也有过 4096 字节的设置后来扩容。容量可以用fcntl(fd, F_SETPIPE_SZ, size)调整前提是有权限且不超过系统上限/proc/sys/fs/pipe-max-size。为什么要默认这个大小太小了频繁阻塞影响吞吐太大了内存占用高64KB 在“吞吐”和“内存开销”之间是个相对均衡的默认值。如果进程一次性写入超过 64KB 的数据且没有读取方write 就会阻塞在写端等读端消费腾出空间。3.2 写入原子性与PIPE_BUF这是匿名管道里最容易被忽视、面试也最常考的一个点PIPE_BUF。POSIX 标准规定向管道写入数据时如果写入字节数不超过PIPE_BUFLinux 上是 4096 字节那么这次写入是原子的。多个进程同时写同一个管道时小于等于 PIPE_BUF 的写入不会彼此交错大于 PIPE_BUF 的写入则会被拆分不保证原子性可能和其他进程的数据交织在一起。用生活场景类比PIPE_BUF 就像物流公司规定“单件货物轻于一定重量必须整车直发”。只要一件货不超重物流公司保证它不会和其他货混在同一车厢超重的货物会被拆成多车中途就可能混进别的东西。这个特性对多进程同时向管道写数据非常关键。如果想避免消息互相穿插就得保证每次 write 不大于 PIPE_BUF并且自己定义好消息格式否则读端拿到的是一个“拼接体”无法区分边界。我在做多进程日志汇聚时就踩过这个坑三个子进程同时往一个管道写日志单条日志超过 4096 字节结果读出来经常出现半条日志交错的情况。3.3 阻塞规则与EOF下面这四条规则很重要建议直接背下来之后排查很多问题都能用上读一个空管道如果还有写端打开read 阻塞如果没有写端了read 返回 0写一个满管道如果还有读端打开write 阻塞如果读端全关闭了write 触发 SIGPIPE进程默认被杀死读管道不会“消耗”数据产生空洞读多少是多少顺序稳定管道文件描述符的引用计数决定管道的生命周期所有 fd 关闭管道就销毁第 1 条是很多操作卡住的根源。很多人以为“没数据就该返回 0”但管道不是这样只要写端没关完read 会一直等。第 2 条是 broken pipe 报错的来源。4. 实际踩过的坑管道破裂、死锁与描述符泄漏4.1 SIGPIPE管道破裂我遇到过最典型的问题是程序向管道写数据时对方早已关闭读端退出结果程序直接“死了”。比如我们用yes | head -n 1yes 这个进程会无限输出head 拿到一行就退出接着 yes 再往管道写管道已经没有读端了内核就会给它发 SIGPIPE。默认情况下进程收到 SIGPIPE 的处置是直接终止。这就引出处理方式如果你不希望进程因为写管道而悄无声息退出可以忽略 SIGPIPE然后检查 write 的返回值通常会是 EPIPE 错误。但注意忽略 SIGPIPE 后某些库比如 stdio 的 printf 系列可能无法感知管道错误导致程序继续跑着但数据没写出去。所以最简单的策略是要么任由默认处理让程序终止要么忽略信号并每次检查系统调用返回值二选一不要模棱两可。实测代码可以这样验证#include stdio.h #include stdlib.h #include unistd.h #include signal.h #include string.h #include errno.h int main(void) { int fd[2]; pipe(fd); close(fd[0]); // 模拟读端已经关闭 signal(SIGPIPE, SIG_IGN); // 忽略 SIGPIPE ssize_t n write(fd[1], hello, 5); if (n -1) { fprintf(stderr, write failed: %s\n, strerror(errno)); // 输出 Broken pipe } return 0; }输出write failed: Broken pipe这时候程序不至于被信号干掉还能继续做清理工作。这个技巧在守护进程、日志转发这类场景里很实用。4.2 双向通信的死锁另一个坑是死锁。比如父子进程各自往对方管道写一大块数据而每一个管道的缓冲区都被自己的写端写满双方又都在等待对方读就会出现典型的互相等待死锁。用双管道做双向通信时必须谨慎设计数据量如果一次要传的数据超过管道容量64KB双通道同时写满谁都读不了就卡死了。解决方案是要么每次写小块数据边写边读要么用 select/poll/epoll 监控多个 fd保证不会只写不读。这也是教科书里为什么说“用管道做双向 IPC 要小心”。4.3 文件描述符泄漏与read永不返回还有一个隐蔽的问题fork 之后所有 fd 都会被子进程继承。如果代码里创建了多个管道又忘了在子进程里关闭用不到的读端或写端那管道的引用计数就永远降不到零read 永远不会返回 0。最典型的表现是主进程的日志里看到 write 成功了但是对端进程却一直阻塞在读。这个在嵌入式 Linux 环境特别常见——有些产品里一个进程要拉起好几个子进程如果某个子进程 fork 时继承了父进程的管道 fd 没关另一个子进程的 read 就永远等不到 EOF。排查思路一般是看/proc/pid/fd/目录下文件描述符数量再配合 strace 确认阻塞在哪次 read/write。4.4 一套实用的排查链路真正遇到管道问题时我建议按照这个顺序排查先看进程状态ps -ef、ps -o stat如果进程长期处于不可中断或睡眠状态很可能是阻塞在 IO用strace -p pid看它卡在哪个系统调用检查对端进程是否还活着读端和写端的引用计数是否归零查看/proc/pid/fd/和/proc/pid/fdinfo/确认管道的读写端都指向正确的位置这一套下来80% 的管道问题都能定位。5. 匿名管道的典型应用与面试考点5.1 典型应用范式shell 流水线这是最广的应用一个命令的输出接一个命令的输入数据边产出边消费内存占用很低生产者-消费者模型一个进程生产数据、多个进程消费管道自带的阻塞机制天然实现了背压控制分阶段处理处理大文本时把任务拆成 filter/sort/aggregate 多个阶段每个阶段一个进程管道连接系统内部机制CGI 程序、system() 内部实现、守护进程启动初始化都用到了匿名管道在嵌入式 Linux 里匿名管道常用于主控进程和通信进程之间传数据好处是不需要磁盘文件、不需要额外路径进程关系明确。比如设备上主控进程采集传感器数据通过管道发给通信进程再由通信进程封装上报整体结构清晰还避免了共享内存那套锁机制。5.2 面试常考点列几个面试常问的点附带简要答案问题参考答案匿名管道默认容量多大Linux 默认 65536 字节可用 fcntl(F_SETPIPE_SZ) 调整PIPE_BUF 是 4096 字节含义是什么小于等于该值写入原子不与其他写方交错管道 read 返回 0 的条件所有写端都已关闭读端读到管道尾部管道写端在读端关闭后继续写会怎样进程收到 SIGPIPE默认终止匿名管道是半双工还是全双工半双工数据单向流动管道生命周期如何由引用它的所有文件描述符决定全部关闭即销毁这些都是很好的自我检查题。建议合上文章想一遍如果能不看资料答上来这块才算真正过关。5.3 与命名管道FIFO的边界最后提一下匿名管道和命名管道的边界。匿名管道没有路径只能在 fork 出来的父子进程之间通信命名管道FIFO则通过文件系统中的一个名字让两个不相关的进程打开同一个管道进行通信。命名管道本质上和匿名管道用的是同一套内核管道机制只是多了一层文件系统的路径入口。这个我准备下一篇详细写包括 mkfifo、open 的阻塞模式、读写方向约定等。关于匿名管道我实际操作中的体会是它看起来简单但一旦深入就有很多细节决定成败。别再小看那个|了——它是理解 Linux 进程间通信最好的起点。如果这篇对你有帮助下一章我们聊命名管道 FIFO我打算把跨进程文件路径通信的坑也一次性讲透。你可以先自己拉一个 FIFO 试试等下一篇出来了对照着看收获会更大。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。