前言(前情回顾)
进程君(父进程)在开发出匿名管道这门传音术后,解决了和自己孩子(子进程)间的沟通问题,父子关系趋于融洽。和孩子沟通后,进程君发现,自己脱离群众太久了,应该加强和群众的沟通。但是进程君与众位道友间没有血缘关系,无法使用匿名管道进行沟通。于是进程君决定改良匿名管道这种技术,让天下道友都能与自己畅通无阻的沟通,最终产生了一种无暇的传音技术——命名管道(FIFO)。
命名管道
我在上一篇文章中提到过:
“两个没有血缘关系的进程间可以同时打开相同的文件,进程内部分配对应的文件描述符,映射关系记录在PCB中,而两个进程间的fd分配时独立的,也就是fd在多进程中不是唯一的。当然我们也可以在进程1中向文件1写入数据,进程2从文件1中读取数据,构成一个伪管道。”
其实这里面的伪管道就是命名管道的意思。创建一个命名管道就是在Linux文件系统下创建一个特殊的fifo文件。与匿名管道的区别是,匿名管道也是文件,但它对文件系统不可见。而命名管道是一个可以在文件系统中看见的文件。
命名管道的创建:mkfifo()
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
这里的filename是一个字符串,描述了命名管道在文件系统中的路径,当字符串中没有‘/’出现是则管道创建在当前目录下。mode一般为0777,对应一个权限掩码,这里不做重点。直接写入即可。
我们先测试一下这个函数,在这里我创建了三个命名管道分别为demo1,demo2,demo3,编译后运行:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
int res1 = mkfifo("demo1",0777); // 创建于当前路径下
int res2 = mkfifo("./demo2",0777); // 创建于当前路径下
int res3 = mkfifo("../demo3",0777); // 创建于上一级路径下
while(1);
return 0;
}
观察运行前后的工程目录
可以发现运行后我们的工程目录下出现了三个新的文件,这三个文件就是我们的命名管道。这就是命名管道对文件系统是可见的这句话的含义。
相信一些脑洞大开的道友已经有了想法,既然命名管道在文件系统中可见,也就说明所有的进程都可以访问这个文件,那么只要我向这个进程中读写文件,是不是就完成了进程间的通信?
答案是:完全正确
如何实现对文件的读和写?不清楚的道友可以去看我之前写的一篇文章:linux多线(进)程编程——(1)前置知识
我们直接上代码,为了体现出命名管道与匿名管道的区别,这次我们要真正实现在两个程序间通信,所以我们要写两个C语言源文件。
proc1.c:创建命名管道并且向命名管道内写入数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int res1 = mkfifo("fifo_demo",0777);
int wfd = open("fifo_demo", O_WRONLY);
if(wfd < 0) printf("proc1 : fifo creat fail\n");
else printf("proc1 : the fifo fd is : %d\n", wfd);
char send_buf[20];
bzero(send_buf, 20);
memcpy(send_buf, "hello, world!", 14);
while(1) {
printf("proc1 : proc1 is writting\n");
write(wfd,send_buf, 20);
printf("proc1 : proc1 has send the data\n");
sleep(1);
}
close(wfd);
printf("proc1 : fifo is closed\n");
return 0;
}
proc2.c:接收管道内的数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
usleep(500*1000);
int rfd = open("fifo_demo", O_RDONLY);
if(rfd < 0) printf("proc2 : fifo creat fail\n");
else printf("proc2 : the fifo fd is : %d\n", rfd);
char recv_buf[20];
sleep(5);
while(1) {
bzero(recv_buf, 20);
read(rfd,recv_buf, 20);
printf("proc2 : proc2 reviced : %s\n", recv_buf);
sleep(1);
}
close(rfd);
printf("proc2 : fifo is closed\n");
return 0;
}
在命令行中运行程序,其中后面加一个&,表示在后台运行,让出终端
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1&
[1] 9729
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc2
proc1 : the fifo fd is : 3
proc2 : the fifo fd is : 3
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc2 : proc2 reviced : hello, world!
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc2 : proc2 reviced : hello, world!
...
注意:
(1)在两个程序中每次写入和读取的字节数量保持一致。
(2)在程序编写时就应该知道管道的名字。
(3)管道是单工通信,开发时不能又读又写,仅能以只读(O_RDONLY)或者只写(O_WRONLY)打开。
关于管道的偏难怪问题
(1)当管道以只读或只写创建/打开时,进程会被阻塞,直到另一段也以另一种方式创建管道。
例如:进程1中我们以只写(write only)的方式创建了管道,当我们只运行进程1时,不会有任何输出,进程1被阻塞:
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1
# 这里没有任何输出,进程1被阻塞
我们拆分终端,运行进程2,进程2中使用只读(read only)的方式打开管道,当进程2开始运行时进程1马上停止阻塞并开始输出信息:
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1
proc1 : the fifo fd is : 3
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
...
(2)读一个空的管道会出现两种情况:一、当写端口未关闭时,暨未调用close(fd[1])时,进程会被阻塞,等待管道内有信号。二、当写端口关闭时,read函数返回0。
情况一:当写端口未关闭时,暨未调用close(fd[1])时,进程会被阻塞,等待管道内有信号。
我们将proc1.c修改为发送三个数据后停止发送,但是不关闭管道,进程进入死循环
proc1.c:
int main() {
int res1 = mkfifo("fifo_demo",0777);
int wfd = open("fifo_demo", O_WRONLY);
if(wfd < 0) printf("proc1 : fifo creat fail\n");
else printf("proc1 : the fifo fd is : %d\n", wfd);
char send_buf[20];
bzero(send_buf, 20);
memcpy(send_buf, "hello, world!", 14);
int i = 0;
while(i++<3) {
printf("proc1 : proc1 is writting\n");
write(wfd,send_buf, 20);
printf("proc1 : proc1 has send the data\n");
sleep(1);
}
while(1);
close(wfd);
printf("proc1 : the fifo fd is closed\n");
return 0;
}
proc2.c不做修改,仍然是睡眠5秒钟后接收数据。
终端输出为如下所示,proc1在proc2睡眠的5秒钟内向管道内发送了三条消息,之后进入while(1);
死循环,而proc2苏醒后,先讲管道内数据读出后,检测到写端口没有被关闭(调用close(wfd);
),因此proc2进入阻塞状态等待可能来自写端口的消息。
lockin@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1&
[1] 13486
lockin@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc2
proc2 : the fifo fd is : 3
proc1 : the fifo fd is : 3
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc2 : proc2 reviced : hello, world!
proc2 : proc2 reviced : hello, world!
proc2 : proc2 reviced : hello, world!
# 进程2 proc2.c 被阻塞不会有任何输出
情况二:当写端口关闭时,read函数返回0。
我们将proc1.c修改为发送三个数据后停止发送并关闭写端口。
proc1.c:
int main() {
int res1 = mkfifo("fifo_demo",0777);
int wfd = open("fifo_demo", O_WRONLY);
if(wfd < 0) printf("proc1 : fifo creat fail\n");
else printf("proc1 : the fifo fd is : %d\n", wfd);
char send_buf[20];
bzero(send_buf, 20);
memcpy(send_buf, "hello, world!", 14);
int i = 0;
while(i++<3) {
printf("proc1 : proc1 is writting\n");
write(wfd,send_buf, 20);
printf("proc1 : proc1 has send the data\n");
sleep(1);
}
close(wfd);
printf("proc1 : the fifo fd is closed\n");
return 0;
}
接收端proc2.c不做任何修改,仍然是睡眠5秒钟后接收数据。
输出如下所示,和刚刚不同的时,proc1发送完消息后就调用了close(wfd);
关闭了管道,此时proc2将管道读空后,不会阻塞,而是继续读管道,但由于管道为空,因此一直返回空消息。
lockin@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1&
[1] 12138
lockin@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc2
proc2 : the fifo fd is : 3
proc1 : the fifo fd is : 3
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : the fifo fd is closed
proc2 : proc2 reviced : hello, world! # 睡眠5秒钟后开始读数据,此时管道内有数据
proc2 : proc2 reviced : hello, world!
proc2 : proc2 reviced : hello, world!
proc2 : proc2 reviced : # 管道内无数据,返回空
proc2 : proc2 reviced :
proc2 : proc2 reviced :
...
(3)写入数据时,若管道已经满了则进程被阻塞。若管道中没有读端口开启,则会返回一个异常,之后进程被终止。
我们不讨论进程满的情况,我们现在只看后半部分,暨写入数据时读数据端被关闭。
我们将读端proc2.c更改为等待5秒钟后关闭。
int main() {
usleep(500*1000);
int rfd = open("fifo_demo", O_RDONLY);
if(rfd < 0) printf("proc2 : fifo creat fail\n");
else printf("proc2 : the fifo fd is : %d\n", rfd);
char recv_buf[20];
sleep(5);
close(rfd);
printf("proc2 : the fifo is closed\n");
return 0;
}
写端proc1.c恢复到循环发送消息:
int main() {
int res1 = mkfifo("fifo_demo",0777);
int wfd = open("fifo_demo", O_WRONLY);
if(wfd < 0) printf("proc1 : fifo creat fail\n");
else printf("proc1 : the fifo fd is : %d\n", wfd);
char send_buf[20];
bzero(send_buf, 20);
memcpy(send_buf, "hello, world!", 14);
while(1) {
printf("proc1 : proc1 is writting\n");
write(wfd,send_buf, 20);
printf("proc1 : proc1 has send the data\n");
sleep(1);
}
close(wfd);
printf("proc1 : fifo is closed\n");
return 0;
}
终端输出如下,可见在proc2.c中关闭管道的读端口后,写入端口写入数据时会返回一个异常,最后终止进程。完美的印证了:写入数据时,若管道已经满了则进程被阻塞。若管道中没有读端口开启,则会返回一个异常,之后进程被终止。
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc1&
[1] 9365
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$ ./proc2
proc2 : the fifo fd is : 3
proc1 : the fifo fd is : 3
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc1 : proc1 is writting
proc1 : proc1 has send the data
proc2 : the fifo is closed
proc1 : proc1 is writting
[1]+ Broken pipe ./proc1
lol@qingfenfuqin:~/work/linux_study/pipe/fifo$
小结
这节课的知识点:
(1)命名管道的创建方法:mkfifo();
(2)命名管道与文件的关系,如何操作命名管道:read();write();
(3)如何在后台运行一个进程:&。
(4)命名管道与匿名管道的差异,以及对文件系统的可见性。
面试高频考点:
(1)当管道以只读或只写创建/打开时,进程会被阻塞,直到另一段也以另一种方式创建管道
(2)读一个空的管道会出现两种情况:一、当写端口未关闭时,暨未调用close(fd[1])时,进程会被阻塞,等待管道内有信号。二、当写端口关闭时,read函数返回0。
(3)写入数据时,若管道已经满了则进程被阻塞。若管道中没有读端口开启,则会返回一个异常,之后进程被终止。
下一集我们将学习进程间第二中通信方式——共享内存的前置知识:linux多线(进)程编程——(5)虚拟内存与内存映射
结束语
“进程君开发出匿名管道与命名管道后,九天十地的道友终于可以畅通无阻的沟通交流了。”
听完这个修仙界传说,不知不觉间你的识海中也多了一道无暇神通,千里传音术——管道。
祝各位道友早日神功大成。