Linux系统篇24——通信(二):匿名管道 vs 命名管道,进程之间到底怎么“传话”?
进程之间默认谁也看不见谁,操作系统给它们接了一根“水管”——管道。本文从“对讲机”和“公共信箱”讲起,把匿名管道与命名管道的底层原理、代码写法、常见坑和面试考点一次讲透。
项目背景:进程之间,默认谁也看不见谁
每个进程都有自己独立的“内存房间”——地址空间。你在进程 A 里写一个变量,进程 B 完全看不到,这是操作系统刻意做的安全隔离。如果谁都能乱闯别人的内存,系统早就乱了。
但现实中的进程必须协作:
- 终端里敲
ps -ef | grep java,ps 的结果要递给 grep 过滤 - 网关里采集程序算完的数据,要交给上报程序继续处理
- 服务器里一个进程收请求、另一个进程处理业务
怎么办?操作系统在它们之间接了一根“水管”——管道(Pipe)。
技术方案:管道 = 内核缓冲区 + 两个 fd
一句话核心:管道是内核里的一段缓冲区,一头写、一头读,数据先进先出(FIFO),像排队一样有序。
两种管道的本质区别:
- 匿名管道:没有名字,只能给有血缘关系的进程(父子)用,用完即毁
- 命名管道:在文件系统里有一个名字(FIFO 文件),任何进程凭名字都能接头
创建命名管道
# 方式一:mkfifo(推荐)
mkfifo myfifo
# 指定权限创建
mkfifo -m 0666 myfifo
# 方式二:mknod,p 表示管道类型
mknod myfifo p
# 查看:首字符为 p 代表管道文件
ls -l myfifo
# prw-r--r-- 1 user user 0 Aug 19 10:00 myfifo
注意:命名管道是特殊文件,数据存在内核内存,磁盘大小始终为 0。
生活化类比
- 匿名管道 = 家里的对讲机:只有自家人(父子进程)手里有话筒和听筒,外人找不到线也听不到声,用完随手拆
- 命名管道 = 小区公共信箱:挂在楼下,谁都能投信取信,大家互不相识,认的只是“信箱名字”(文件路径)
一句话记忆:匿名管道认“血缘”,命名管道认“名字”。
系统架构:管道底层原理
管道不是真的水管,真身是内核里的一个缓冲区(Linux 上是环形队列),外加两个文件描述符:
写端 fd[1] 读端 fd[0]
进程A ────► [内核环形缓冲区] ────► 进程B
关键点:
- 读写走系统调用:写端 write(),读端 read(),内核负责搬运和同步
- 缓冲区有上限:Linux 管道默认容量约 64KB;写满 → 写端阻塞等待
- 半双工:数据只能单向流;想双向就开两根水管
- FIFO:先进先出,先写先读,不会乱序
- 原子性:写入量 ≤ PIPE_BUF(通常 4096 字节)时一次写完,不会被其他写者拆散
实施过程
匿名管道:pipe() + fork() 经典组合
pipe() 一次性返回两个 fd:fd[0] 读端、fd[1] 写端。fork() 后父子共享这对 fd。为了让数据单向流动,要“各关一头”:
- 子进程:关掉读端 fd[0],只往 fd[1] 写
- 父进程:关掉写端 fd[1],只从 fd[0] 读
#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 字节)
为什么非要“关一头”?
- 形成单向流:不关的话两边都能读能写,逻辑混乱
- read 正确结束:read() 只有所有写端都关闭时才返回 0(EOF);子进程不关写端,父进程会一直阻塞
- 避免脏数据:父进程自己写自己读,数据就乱了
匿名管道两大限制
- 必须有血缘关系:fd 是进程私有资源,无名管道没有文件系统“门牌号”,不相关进程找不到它
- 生命周期短:进程一退出管道就没了
命名管道:mkfifo() 靠名字接头
创建后在文件系统真实存在一个 p 类型文件,两个互不相识的进程只要都能访问该路径就能通信,不需要血缘关系。
#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;
}
关键坑:open() 的阻塞
命名管道 open() 本身就会阻塞:
- 先 open(path, O_RDONLY) 的进程会卡住,直到有进程 open(path, O_WRONLY)
- 反过来先开写端也一样
所以独立进程通信时要约定好“谁先开、开哪头”。
真正的用途:两个独立进程通信
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 阻塞,再启动另一端,这就是“管道两侧不同步”的经典协作模型。
管道通信的四种情况
| 情况 | 现象 |
|---|---|
| 读正常,写满 | write 阻塞 |
| 写正常,读空 | read 阻塞 |
| 写端全部关闭,读正常 | read 读完返回 0 |
| 读端全部关闭,写正常 | 收到 SIGPIPE,写进程终止 |
阻塞、非阻塞与 SIGPIPE
管道读写默认阻塞模式:
| 场景 | 写端行为 | 读端行为 |
|---|---|---|
| 缓冲区有空位 | 正常写入 | 正常读出 |
| 缓冲区写满 | 阻塞等待,直到有空间 | — |
| 没有数据可读 | — | 阻塞等待,直到有数据 |
| 对端全部关闭 | 收到 SIGPIPE,进程被杀 | read() 返回 0(EOF) |
SIGPIPE 是必考题也是必踩的坑:没人读你还在写,系统直接发信号崩掉进程。经典例子:
$ yes | head -1
y
yes 疯狂输出,head 读完一行关闭读端,yes 立刻被 SIGPIPE 杀死。想避免被杀,忽略信号:
#include <signal.h>
signal(SIGPIPE, SIG_IGN); // 忽略信号,write() 返回 -1 而不是崩溃
新手必踩的四个坑
- 忘记关多余的 fd:子进程不关读端、父进程不关写端,read() 永远读不到 EOF,程序死锁
- 写入超过 PIPE_BUF:超过 4096 字节的写入不是原子的,多写者场景数据可能交叉
- 命名管道 open 顺序错:只有一端开是开不了的,会永久阻塞
- 用完不 unlink:命名管道文件不会自动消失,不清理会在 /tmp 积累垃圾
应用价值:工业场景里的管道
- 嵌入式 Linux 网关:采集进程 → 协议转换 → 数据上报,用管道串成流水线,进程解耦、互不阻塞
- 自动化运维脚本:ps | grep、日志过滤、统计排序,一行命令就是一条管道流水线
- 本地轻量协作:无需网络的进程间 C/S 模型,命名管道天然支持“多对一”
管道是理解后续所有 IPC 的地基——文件描述符、阻塞、缓冲区、同步这些概念,在共享内存和 Socket 里全部还会再遇到。把这根水管玩明白,后面的路会顺很多。
SEO关键词
Linux 匿名管道、命名管道、mkfifo、pipe、进程间通信、IPC、嵌入式Linux、管道阻塞、SIGPIPE、Linux系统篇
