Linux系统篇24——通信(二):匿名管道 vs 命名管道:进程之间到底怎么“传话“?
2026/8/22 17:01:13 网站建设 项目流程

你在终端敲下ps -ef | grep java时,有没有想过那个竖线|到底是什么?为什么ps的输出能"流"进grep的嘴里?其实这背后,藏着一套操作系统最经典的设计——进程间通信(IPC)。而管道,就是其中最朴素、最古老,也最好懂的一种。本文不堆概念、不抄书,从"对讲机"和"公共信箱"讲起,把匿名管道命名管道的底层原理、代码写法、常见坑、面试考点一次讲透。让你彻底告别"看过就忘"。



一、先说痛点:进程之间,默认谁也看不见谁

每个进程都有自己独立的"内存房间"——地址空间。你在房间 A 里写了个变量,房间 B 里的进程是完全看不到的,这是操作系统为了安全刻意隔离的。要是谁都能乱闯别人的内存,那系统早就乱套了。

但现实里进程又必须合作:

  • 终端敲ps -ef | grep javaps的结果要递给grep过滤
  • 程序 A 算完的数据要交给程序 B 接着处理
  • 服务器要一个进程接收请求、另一个进程处理业务

怎么办?操作系统给它们之间接了一根水管——这就是管道(Pipe)

二、极简核心结论

管道 = 内核里的一段缓冲区,一头写、一头读,数据先进先出(FIFO),像排队一样有序。

管道分两种,本质区别就一句话:

  1. 匿名管道:没名没姓,只能给有"血缘关系"的进程(父子)用,用完即毁。
  2. 命名管道:在文件系统里有一个名字(FIFO 文件),任何进程凭名字都能来接头。

2.1 管道类型文件的创建与查看

# 1.创建命名管道 #方式1 mkfifo(推荐) mkfifo myfifo # 指定权限创建管道文件 mkfifo -m 0666 myfifo #方式2 mknod,p表示管道类型 mknod myfifo p # 2.查看管道文件类型,输出首字符p代表管道文件 ls -l myfifo # file命令识别管道文件 file myfifo # 命名管道:特殊文件类型p,用于进程间通信,数据存放内存,磁盘大小始终0 prw-r--r-- 1 user user 0 Aug 19 10:00 myfifo

三、生活化类比:一秒钟记住两种管道

  • 匿名管道 = 家里的对讲机:只有自家人(父子进程)手里有话筒和听筒,外人既找不到线、也听不到声,用完随手就拆。
  • 命名管道 = 小区公共信箱:挂在楼下,谁都能投信、谁都能取信,大家不认识彼此,认的只是"信箱名字"(文件路径)。不想用了,可以拆,也可以一直留着。

一句话记忆:匿名管道认"血缘",命名管道认"名字"。


四、一张表看懂两者区别

对比项匿名管道命名管道
名字有(FIFO 文件路径)
谁能用仅父子 / 亲缘进程任意进程
创建方式pipe()mkfifo()
生命周期用完即毁,进程退出就没了存在于文件系统,手动unlink删除
是否像普通文件不是,只在内核里是,可以ls看到,open打开
典型场景命令行ps | grep、父子协作多个独立程序、服务与客户端

五、先搞懂底层:管道到底是个什么东西

管道不是真的"水管",它的真身是内核里的一个缓冲区(Linux 上是一个环形队列),外加两个文件描述符(fd)

写端 fd[1] 读端 fd[0] 进程 A ──────► [内核环形缓冲区] ──────► 进程 B

关键点:

  1. 一切读写走系统调用:写端用write(),读端用read(),内核负责搬运和同步。
  2. 缓冲区有上限:Linux 管道默认容量约64KB。写端写太快、读端没跟上 → 写满 → 写端阻塞等待。
  3. 半双工:数据只能单向流。想双向?开两根水管,各走各的。
  4. FIFO:先进先出,先写进去的先被读出,像排队一样,不会乱序。
  5. 原子性(一个要记的关键词):当写入的数据量 ≤PIPE_BUF(通常 4096 字节)时,这次写入是原子的——要么一次写完,要么不写,不会被其他写者拆散。多个进程同时写小数据不会互相插队。

六、匿名管道:pipe()+fork()经典组合

6.1 工作原理

pipe()一次性返回两个文件描述符:

  • fd[0]:读端(只能读)
  • fd[1]:写端(只能写)

接着fork()让父子进程共享这对 fd。此时一份数据,两个进程共用。为了让数据单向流动,要"各关一头":

  • 子进程:关掉读端fd[0],只往fd[1]
  • 父进程:关掉写端fd[1],只从fd[0]

6.1.1 站在文件描述符角度‑深度理解管道

6.1.2 站在内核角度-管道本质

Linux下一切皆文件

6.2 完整可运行代码

#include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/wait.h> int main() { int fd[2]; pipe(fd); // ① 建水管 pid_t pid = fork(); // ② 分裂:父子共享同一根水管 if (pid == 0) { // 子进程:负责"写" close(fd[0]); // 关掉读端 const char *msg = "hello from child"; write(fd[1], msg, strlen(msg)); close(fd[1]); // 写完关写端 return 0; } // 父进程:负责"读" close(fd[1]); // 关掉写端 char buf[64] = {0}; int n = read(fd[0], buf, sizeof(buf) - 1); printf("父进程收到: %s (%d 字节)\n", buf, n); close(fd[0]); wait(NULL); // 等子进程收尸,避免僵尸 return 0; }

编译运行:

gcc pipe_demo.c -o pipe_demo && ./pipe_demo # 输出: 父进程收到: hello from child (17 字节)

6.3 为什么非要"关一头"?

这是新手最容易懵的地方。原因有三:

  1. 形成单向流:不关的话两边都能读能写,逻辑混乱。
  2. read 能正确结束:读端read()只有在所有写端都被关闭时,才会读到 0(EOF)返回。如果子进程没关写端,父进程read()会一直阻塞等数据。
  3. 避免脏数据:万一父进程也往写端写,自己读到自己写的东西,就乱套了。

6.4 匿名管道的两大限制

  1. 必须要有血缘关系:fd 是进程私有资源,无名管道没有文件系统的"门牌号",不相关的进程根本无法找到它。
  2. 生命周期短:进程一退出,管道就没了,不能长期存放数据。

七、命名管道:mkfifo(),靠"名字"接头

7.1 和匿名管道最大区别

命名管道创建后,在文件系统里会真实存在一个文件(类型显示为p,即 FIFO):

$ mkfifo /tmp/myfifo $ ls -l /tmp/myfifo prw-r--r-- 1 user user 0 8月 19 10:00 /tmp/myfifo # 注意开头是 p

有了这个名字,两个互不相识的进程,只要都能访问这个路径,就能通信——不需要任何血缘关系。

7.2 完整可运行代码(父子版,先跑通)

#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <sys/wait.h> #include <string.h> int main() { const char *path = "/tmp/myfifo"; mkfifo(path, 0666); // 建一根"有名字"的水管 pid_t pid = fork(); if (pid == 0) { // 子进程:写 int fd = open(path, O_WRONLY); const char *msg = "hello from writer"; write(fd, msg, strlen(msg)); close(fd); return 0; } // 父进程:读 int fd = open(path, O_RDONLY); char buf[64] = {0}; int n = read(fd, buf, sizeof(buf) - 1); printf("父进程收到: %s (%d 字节)\n", buf, n); close(fd); wait(NULL); unlink(path); // 用完拆掉水管 return 0; }

7.3 关键坑:open() 的阻塞

命名管道有一个非常经典的坑:open()本身就会阻塞。

  • open(path, O_RDONLY)的进程,会卡住等,直到有别的进程open(path, O_WRONLY)出现。
  • 反过来,先打开写端的也一样。

所以上面的代码里必须先有读者、后有写者(或反过来),否则程序会卡在open上。这也解释了为什么两个独立进程要通信时,通常要约定好"谁先开、开哪头"。

7.4 真正的用途:两个独立进程通信

命名管道的价值在"非亲缘进程"。两个独立程序:

writer.c(终端 1 运行):

#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { const char *path = "/tmp/myfifo"; mkfifo(path, 0666); int fd = open(path, O_WRONLY); // 等读者出现才返回 char buf[128]; while (fgets(buf, sizeof(buf), stdin)) { write(fd, buf, strlen(buf)); } close(fd); return 0; }

reader.c(终端 2 运行):

#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { const char *path = "/tmp/myfifo"; int fd = open(path, O_RDONLY); // 等写者出现才返回 char buf[128]; int n; while ((n = read(fd, buf, sizeof(buf))) > 0) { write(STDOUT_FILENO, buf, n); // 打印到屏幕 } close(fd); return 0; }
# 终端 1 gcc writer.c -o writer && ./writer # 终端 2 gcc reader.c -o reader && ./reader # 在终端 1 敲一行字回车,终端 2 立刻显示

注意:必须先启动一个进程让它open阻塞,再启动另一个。这就是"管道两侧不同步"的经典协作模型。


八、管道通信 4 种情况

情况现象
读正常,写满write阻塞
写正常,读空read阻塞
写全部关闭,读正常read 读完返回0
读全部关闭,写正常收到SIGPIPE,写进程终止

九、阻塞、非阻塞与 SIGPIPE:管道会"杀人"

管道用的读/写默认都是阻塞模式,行为对照如下:

场景写端行为读端行为
缓冲区有空位正常写入正常读出
缓冲区写满阻塞等待,直到有空间
没有数据可读阻塞等待,直到有数据
对端全部关闭收到SIGPIPE信号,进程直接被杀read()返回 0(EOF)

这个SIGPIPE是必考题也是必踩的坑:没人读你还在写,系统会直接一发信号崩掉你的进程。经典例子:

$ yes | head -1 y

yes疯狂输出,head读完一行就关闭读端,yes立刻被SIGPIPE杀死——shell 里就是这么自然地"杀水管"。

想避免被杀死?忽略信号:

#include <signal.h> signal(SIGPIPE, SIG_IGN); // 忽略信号,让 write() 返回 -1 而不是崩溃

十、新手必踩的 4 个坑

  1. 忘记关多余的 fd:子进程不关读端、父进程不关写端,read()永远不会读到 EOF,程序死锁。
  2. 写入超过 PIPE_BUF:超过 4096 字节的写入不是原子的,多写者场景下数据可能交叉。
  3. 命名管道 open 顺序错:先写后读或先读后写都行,但只有一端是开不了的,会永久阻塞。
  4. 用完不 unlink:命名管道文件不会自动消失,不清理会留在/tmp里积累垃圾。

十一、多个进程如何访问同一个文件

先思考一个问题:如果两个进程同时访问同一个文件,操作系统会不会在内存中将这份文件加载两次?

答案是不会 为什么呢?因为没必要

文件共享原理:多个进程通过独立的文件描述符和 struct file,最终指向同一个 inode,实现对同一文件数据的共享访问。

十二、历史背景:管道符号|的故事

管道是 Unix 最古老也最优雅的设计之一。1964 年,Douglas McIlroy 在贝尔实验室提出了"把一个程序的输出接到另一个程序的输入"的想法。真正落地是在 1972 年,Ken Thompson一夜之间给 Unix 加上了管道机制和|符号。

当时他们甚至不需要新系统调用——用已有的openreadwrite就拼出来了,这就是 Unix"一切皆文件"哲学的体现:管道本质也是一种"文件",只是没名字。

有了|,那句著名的 Unix 哲学"一个程序只做一件事,并且做好"才真正有了生命力——复杂任务可以用小工具组合出来,比如:

# 统计当前目录下最大文件的占用 ls -l | sort -k5 -rn | head -1 # 查看最常见的 10 个词 cat file.txt | tr -s ' ' '\n' | sort | uniq -c | sort -rn | head -10

每一根|就是一根匿名管道,ls是生产端,head是消费端,中间是流水线。


十三、面试考点速查(就业向)

考点答案一句话
管道是什么内核缓冲区 + 两个 fd,FIFO,半双工
匿名/命名区别匿名要血缘、无名字、用完即毁;命名有路径、任意进程可连
半双工怎么解决开两根管道,各传一个方向
read 什么时候返回 0所有写端都被关闭时(EOF)
写端没人读会怎样收到 SIGPIPE,进程被杀
管道缓冲区多大Linux 默认约 64KB;原子写上限 PIPE_BUF≈4096
为什么 open 命名管道会卡住open 要等对端出现,双向握手

十四、拓展:IPC 全家桶,管道排第几?

管道是 IPC 的入门课,但绝不是万能钥匙。把整个家族摆一起看:

通信方式一句话本质优点缺点适用场景
管道单向字节流最简单、安全半双工、容量小父子进程、流式数据
消息队列按"消息块"传,像传纸条有边界、可多进程数据要拷贝两次需要分块的短消息
共享内存直接共用一块内存最快,零拷贝要自己加锁同步大数据、高频通信
信号只发"提醒",不带数据极轻量携带信息少事件通知
信号量计数器,管"资源个数"天然的锁不传数据多进程互斥同步
Socket跨网络通信跨机器开销大网络编程、C/S 架构

一句话总结:管道是理解后续所有 IPC 的地基——文件描述符、阻塞、缓冲区、同步这些概念,在共享内存和 Socket 里全都还会再遇到。把这根水管玩明白,后面的路会顺很多。


文末彩蛋echo "hi" | grep hi | wc -l这一行命令里,一共用了多少根管道?答案:2 根——三个进程,两根水管串成流水线。数对了吗?😉

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询