尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux系统篇24——通信(二):匿名管道 vs 命名管道:进程之间到底怎么“传话“?

Linux系统篇24——通信(二):匿名管道 vs 命名管道:进程之间到底怎么“传话“? 你在终端敲下ps -ef | grep java时有没有想过那个竖线|到底是什么为什么ps的输出能流进grep的嘴里其实这背后藏着一套操作系统最经典的设计——进程间通信IPC。而管道就是其中最朴素、最古老也最好懂的一种。本文不堆概念、不抄书从对讲机和公共信箱讲起把匿名管道和命名管道的底层原理、代码写法、常见坑、面试考点一次讲透。让你彻底告别看过就忘。一、先说痛点进程之间默认谁也看不见谁每个进程都有自己独立的内存房间——地址空间。你在房间 A 里写了个变量房间 B 里的进程是完全看不到的这是操作系统为了安全刻意隔离的。要是谁都能乱闯别人的内存那系统早就乱套了。但现实里进程又必须合作终端敲ps -ef | grep javaps的结果要递给grep过滤程序 A 算完的数据要交给程序 B 接着处理服务器要一个进程接收请求、另一个进程处理业务怎么办操作系统给它们之间接了一根水管——这就是管道Pipe。二、极简核心结论管道 内核里的一段缓冲区一头写、一头读数据先进先出FIFO像排队一样有序。管道分两种本质区别就一句话匿名管道没名没姓只能给有血缘关系的进程父子用用完即毁。命名管道在文件系统里有一个名字FIFO 文件任何进程凭名字都能来接头。2.1 管道类型文件的创建与查看# 1.创建命名管道 #方式1 mkfifo推荐 mkfifo myfifo # 指定权限创建管道文件 mkfifo -m 0666 myfifo #方式2 mknodp表示管道类型 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关键点一切读写走系统调用写端用write()读端用read()内核负责搬运和同步。缓冲区有上限Linux 管道默认容量约64KB。写端写太快、读端没跟上 → 写满 → 写端阻塞等待。半双工数据只能单向流。想双向开两根水管各走各的。FIFO先进先出先写进去的先被读出像排队一样不会乱序。原子性一个要记的关键词当写入的数据量 ≤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 为什么非要关一头这是新手最容易懵的地方。原因有三形成单向流不关的话两边都能读能写逻辑混乱。read 能正确结束读端read()只有在所有写端都被关闭时才会读到 0EOF返回。如果子进程没关写端父进程read()会一直阻塞等数据。避免脏数据万一父进程也往写端写自己读到自己写的东西就乱套了。6.4 匿名管道的两大限制必须要有血缘关系fd 是进程私有资源无名管道没有文件系统的门牌号不相关的进程根本无法找到它。生命周期短进程一退出管道就没了不能长期存放数据。七、命名管道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()返回 0EOF这个SIGPIPE是必考题也是必踩的坑没人读你还在写系统会直接一发信号崩掉你的进程。经典例子$ yes | head -1 yyes疯狂输出head读完一行就关闭读端yes立刻被SIGPIPE杀死——shell 里就是这么自然地杀水管。想避免被杀死忽略信号#include signal.h signal(SIGPIPE, SIG_IGN); // 忽略信号让 write() 返回 -1 而不是崩溃十、新手必踩的 4 个坑忘记关多余的 fd子进程不关读端、父进程不关写端read()永远不会读到 EOF程序死锁。写入超过 PIPE_BUF超过 4096 字节的写入不是原子的多写者场景下数据可能交叉。命名管道 open 顺序错先写后读或先读后写都行但只有一端是开不了的会永久阻塞。用完不 unlink命名管道文件不会自动消失不清理会留在/tmp里积累垃圾。十一、多个进程如何访问同一个文件先思考一个问题如果两个进程同时访问同一个文件操作系统会不会在内存中将这份文件加载两次答案是不会 为什么呢因为没必要文件共享原理多个进程通过独立的文件描述符和 struct file最终指向同一个 inode实现对同一文件数据的共享访问。十二、历史背景管道符号|的故事管道是 Unix 最古老也最优雅的设计之一。1964 年Douglas McIlroy 在贝尔实验室提出了把一个程序的输出接到另一个程序的输入的想法。真正落地是在 1972 年Ken Thompson一夜之间给 Unix 加上了管道机制和|符号。当时他们甚至不需要新系统调用——用已有的open、read、write就拼出来了这就是 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是消费端中间是流水线。十三、面试考点速查就业向考点答案一句话管道是什么内核缓冲区 两个 fdFIFO半双工匿名/命名区别匿名要血缘、无名字、用完即毁命名有路径、任意进程可连半双工怎么解决开两根管道各传一个方向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 根——三个进程两根水管串成流水线。数对了吗
返回列表