🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然
🏠博主简介
文章目录
- 前言
- 一、匿名管道缺的不是“管道”,而是一个双方都认识的入口
- 二、先用命令把 FIFO 的行为跑出来
- 2.1 `mkfifo`
- 2.2 为什么 `echo > fifo` 会卡住
- 三、FIFO 的 `open()` 规则比 `read()` 更早开始同步
- 四、系统调用 `mkfifo()`
- 五、用命名管道写一个最小 Server / Client
- 5.1 Server
- 5.2 Client
- 六、Client 一退出,Server 为什么会读到 `0`
- 七、匿名管道和命名管道到底差在哪
- 八、几个容易理解错的地方
- 1. “命名管道是普通磁盘文件”
- 2. “只要文件路径相同,就一定是同一个打开文件对象”
- 3. Server 卡住一定是 `read()`
- 4. `read()` 读出来就是字符串
- 总结
前言
前面学匿名管道时有一个很明显的限制:
父进程先 pipe()
↓
fork()
↓
子进程继承父进程的 fd
↓
父子进程看到同一条管道
这套方法很好用,但前提是两个进程之间能通过fork()建立文件描述符的继承关系。
如果现在有两个完全独立启动的程序:
./server ./client它们没有父子关系,也没有谁能把自己已经打开的匿名管道 fd “继承”给另一方。
那怎么让它们找到同一份内核通信资源?
命名管道解决的正是“两个无亲缘关系进程怎样找到同一份通信资源”这个问题。
一、匿名管道缺的不是“管道”,而是一个双方都认识的入口
匿名管道没有可供无关进程重新打开的路径。
intpipefd[2];pipe(pipefd);pipe()返回的pipefd[0]、pipefd[1]只对当前进程以及后来继承这些 fd 的进程有意义。
两个毫无关系的进程不可能提前知道对方进程里的某个 fd 是多少。
命名管道换了一种思路:
不再靠 fd 继承找到同一条管道,而是让两个进程约定同一个 pathname。
例如:
/tmp/myfifoServer 和 Client 都去open("/tmp/myfifo", ...),内核就能把它们连接到同一个 FIFO 对象上。
这里有个地方很容易理解偏。
命名管道确实在文件系统中拥有路径、目录项和文件类型,执行ls -l能看到它;但真正传输的数据并不会像普通文件一样写进磁盘文件内容。
这里记住:
路径存在于文件系统中 ↓ 用于让不同进程找到同一个 FIFO ↓ 通信数据仍然在内核管道机制中流动所以“有文件名”不等于“通信数据会落盘”。
二、先用命令把 FIFO 的行为跑出来
2.1mkfifo
创建:
mkfifofifo查看:
ls-lfifo命名管道的文件类型通常会显示为:
p删除时直接:
rmfifo或者:
unlink fifo没必要写:
rm-rffifofifo不是目录,-r在这里没有意义。
2.2 为什么echo > fifo会卡住
先执行:
echo"hello fifo">fifo如果此时没有任何进程以读方式打开 FIFO,Shell 会停在那里。
另开一个终端:
catfifo此时两边都会继续运行,cat能读到:
hello fifo这一步最值得观察的是:
写端 open(O_WRONLY) │ │ 没有读端时等待 ▼ 读端 open(O_RDONLY) │ ▼ 双方建立通信关系三、FIFO 的open()规则比read()更早开始同步
默认阻塞模式下:
| 打开方式 | 另一端不存在时 |
|---|---|
open(..., O_RDONLY) | 等待某个写端打开 |
open(..., O_WRONLY) | 等待某个读端打开 |
也就是说,阻塞甚至可能发生在open(),代码还没有走到read()或write()。
如果使用O_NONBLOCK,规则会变化:
O_RDONLY | O_NONBLOCK可以立即打开。
而:
O_WRONLY | O_NONBLOCK如果一个读端都不存在,会失败并得到ENXIO。
排查 FIFO “程序一启动就卡住”时,先看open()有没有返回。
很多时候不是read()卡住,而是:
open(FIFO_FILE,O_RDONLY);根本还没有返回。
四、系统调用mkfifo()
程序中创建 FIFO:
#include<sys/types.h>#include<sys/stat.h>intmkfifo(constchar*pathname,mode_t mode);成功返回:
0失败返回:
-1例如:
#include<cerrno>#include<cstdio>#include<sys/stat.h>constchar*FIFO_FILE="./myfifo";intmain(){umask(0);if(mkfifo(FIFO_FILE,0666)<0){if(errno!=EEXIST){perror("mkfifo");return1;}}return0;}Server 可能被重复启动,所以遇到EEXIST时不要先认定程序出错。
如果路径已经存在,还应该确认它是不是我们期望的 FIFO,而不是同名普通文件。
五、用命名管道写一个最小 Server / Client
Server:读端 Client:写端5.1 Server
#include<cerrno>#include<cstdio>#include<cstdlib>#include<fcntl.h>#include<iostream>#include<sys/stat.h>#include<unistd.h>constchar*FIFO_FILE="./myfifo";intmain(){umask(0);if(mkfifo(FIFO_FILE,0666)<0&&errno!=EEXIST){perror("mkfifo");return1;}std::cout<<"等待 client 打开 FIFO..."<<std::endl;intfd=open(FIFO_FILE,O_RDONLY);if(fd<0){perror("open");unlink(FIFO_FILE);return2;}charbuffer[1024];while(true){ssize_t n=read(fd,buffer,sizeof(buffer)-1);if(n>0){buffer[n]='\0';std::cout<<"client say# "<<buffer<<std::endl;}elseif(n==0){std::cout<<"所有写端已经关闭,server 结束读取"<<std::endl;break;}else{perror("read");break;}}close(fd);unlink(FIFO_FILE);return0;}这里顺手修正原稿中的一个细节。原来写的是:
buffer[n-1]=0;改成:
buffer[n]='\0';read()返回的n就是本次实际读到的有效字节数。
如果你的目的只是给 C 字符串补结束符,应该放在有效数据后一个位置。
只有你明确知道最后一个字节一定是换行符,并且想把换行删掉时,才考虑改buffer[n - 1]。
5.2 Client
#include<cstdio>#include<fcntl.h>#include<iostream>#include<string>#include<unistd.h>constchar*FIFO_FILE="./myfifo";intmain(){intfd=open(FIFO_FILE,O_WRONLY);if(fd<0){perror("open");return1;}intcount=1;pid_t pid=getpid();while(true){std::cout<<"Please Enter# ";std::string message;if(!std::getline(std::cin,message)){break;}message+=", message number "+std::to_string(count++)+" [pid="+std::to_string(pid)+"]";ssize_t n=write(fd,message.c_str(),message.size());if(n<0){perror("write");break;}}close(fd);return0;}运行:
./server再打开另一个终端:
./client六、Client 一退出,Server 为什么会读到0
假设 Client 正常退出:
close(fd);如果系统中已经不存在任何写端,那么 Server 继续:
read(fd,buffer,sizeof(buffer));当 FIFO 中剩余数据已经全部读完后,read()返回:
0这就是 EOF。
这里和匿名管道是一套规则:
有没有路径,只决定双方怎么“找到”管道 读写端、EOF、SIGPIPE、字节流规则,仍然属于管道语义七、匿名管道和命名管道到底差在哪
匿名管道和命名管道的区别不用背很多条,抓住“双方怎样找到同一条管道”最重要。
| 对比 | 匿名管道 | 命名管道 FIFO |
|---|---|---|
| 创建方式 | pipe() | mkfifo() |
| 是否有路径 | 没有 | 有 |
| 常见使用关系 | 父子等有亲缘关系进程 | 无亲缘关系进程也可以 |
| 数据是否落盘 | 否 | 否 |
| 数据形式 | 字节流 | 字节流 |
| 阻塞、EOF、SIGPIPE | 有 | 有 |
核心区别:
匿名管道通常靠fork()继承 fd;
命名管道靠双方打开同一个 pathname 建立联系。
八、几个容易理解错的地方
1. “命名管道是普通磁盘文件”
不对。
它在文件系统中有名字和类型,但 FIFO 中传递的字节并不是普通文件内容。
2. “只要文件路径相同,就一定是同一个打开文件对象”
也不能这么说。
多个open()会形成各自的打开实例和 fd,底层文件系统对象存在共享关系,但不要简单写成“两个进程的struct file完全是同一个”。
3. Server 卡住一定是read()
不一定。
读端:
open(path,O_RDONLY);在没有写端时就可能已经阻塞。
4.read()读出来就是字符串
不是。
read()只认识字节,不认识 C 字符串结尾的'\0'。
所以:
buffer[n]='\0';是用户代码自己补的。
总结
命名管道没有改变管道传输数据的本质,它只是解决了匿名管道最麻烦的那个问题:
两个没有亲缘关系的进程 到底怎样找到同一条管道?答案就是 pathname。
Server 和 Client 约定:
./myfifo各自调用open(),就能连接到同一条 FIFO。
最后把这篇真正要记住的内容收一下:
mkfifo()创建的是 FIFO 特殊文件;- FIFO 数据不会作为普通文件内容刷到磁盘;
- 阻塞可能从
open()就开始; read()返回0表示所有写端关闭并且剩余数据已经读完;- 命名管道和匿名管道的主要区别,是“双方怎样找到同一资源”。
下一篇继续沿着“让不同进程看到同一份资源”往下走,不过不再借助文件描述符,而是直接把同一批物理页映射进多个进程的地址空间——System V 共享内存。
资源分享:
【Linux】从匿名管道到进程池:任务派发、fd 继承 Bug 与完整实现
【Linux】进程为什么不能直接通信?匿名管道原理、fd 继承与读写规则
【Linux】没有公网 IP 也能 SSH 回内网:反向 SSH、ProxyJump 与 systemd 保活实战