linux多线(进)程编程——(4)进程间的传音术(命名管道)

前言(前情回顾)

进程君(父进程)在开发出匿名管道这门传音术后,解决了和自己孩子(子进程)间的沟通问题,父子关系趋于融洽。和孩子沟通后,进程君发现,自己脱离群众太久了,应该加强和群众的沟通。但是进程君与众位道友间没有血缘关系,无法使用匿名管道进行沟通。于是进程君决定改良匿名管道这种技术,让天下道友都能与自己畅通无阻的沟通,最终产生了一种无暇的传音技术——命名管道(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)虚拟内存与内存映射

结束语

“进程君开发出匿名管道与命名管道后,九天十地的道友终于可以畅通无阻的沟通交流了。”
听完这个修仙界传说,不知不觉间你的识海中也多了一道无暇神通,千里传音术——管道。
祝各位道友早日神功大成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值