34-进程间通信——FIFO(命名管道)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82843881

1. 何为命名管道


   管道(pipe)只能用于“有血缘关系”的进程间。

   pipe与FIFO之间最大的区别就是FIFO提供一个路径名与之关联,在文件系统中有一个索引块,以文件路径的形式存在(在磁盘中没有数据块,所有数据都存放在内核),而这个文件路径是FIFO被称为命名管道的重要原因。

   FIFO与管道类似,其本质上也是内核的一块缓冲区,FIFO也有一个写入端和读取端,FIFO中的数据读写顺序和管道PIPE中是一样的。进程就像打开普通文件一样,调用open函数打开FIFO文件进行读写操作,实现任意两个没有关系的进程通信。


2. 使用mkfifo函数创建FIFO

mkfifo函数就是用于创建一个FIFO文件

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname,  mode_t mode);  

返回值说明:成功返回0; 失败返回-1,设置errno

参数pathname:要创建的FIFO文件名且该文件必须不存在

参数mode:指定FIFO文件的权限


调用mkfifo函数创建FIFO进行进程间通信的大概过程:
在这里插入图片描述

   如图所示,当调用mkfifo函数创建一个名为myfifo的FIFO文件时,任何有权限的进程都能打开myfifo这个文件。

   使用FIFO进行进程间通信的时候,通常会设置一个写入进程和读取进程,例如A进程通过open函数以只写方式打开FIFO文件,并通过文件描述符把数据写入FIFO内核缓冲区,B进程也通过open函数以只读方式打开FIFO文件,通过文件描述符从FIFO的缓冲区中读取数据。


   还使用命令创建管道方式,例如:mkfifo test,其实mkfifo命令本质上就是调用了mkfifo函数来创建FIFO文件。
在这里插入图片描述

   我们可以看到创建的FIFO文件test,另外在文件权限位的最前面的p就表示文件类型,即管道文件。


3. 使用FIFO进行进程通信

   一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo,如:close、read、write、unlink等,但不能使用lseek函数。


实验:fifo1程序创建FIFO文件test,然后打开test文件写数据,fifo2程序打开test文件读数据


fifo1写数据

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd = 0;
        int len = 0;
        int ret;
        char buf[1024] = {0};

        //创建FIFO文件
        ret = mkfifo("test" , 0664);
        if(ret != 0){
                perror("mkfifo error:");
                exit(-1);
        }

        //只写方式打开test
        fd = open("test", O_WRONLY);
        if(fd < 0){
                perror("open error");
        }
        puts("open fifo write");
        //向FIFO写入数据
        while((len = read(STDIN_FILENO, buf, sizeof(buf))) > 0){
                write(fd, buf, len);
        }
        close(fd);
        return 0;
}

fifo2读数据

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd = 0;
        int len = 0;
        char buf[1024] = {0};

        //只读打开test
        fd = open("test", O_RDONLY);
        if(fd < 0){
                perror("open error:");
        }
        puts("open fifo read");

        //从管道中读取数据
        while((len = read(fd, buf, sizeof(buf))) > 0){
                write(STDOUT_FILENO, buf, len);
        }
        
        //如果read返回0,说明读到文件末尾或对端已关闭
        if(len == 0){
                puts("peer is close or file end");
        }else{
                puts("read fifo");
        }

        close(fd);
        return 0;
}

程序执行结果:
1 . 当执行./fifo1时会出现阻塞,因为./fifo2没有打开


2 . 接着再执行./fifo2后,程序执行结果如图所示:
在这里插入图片描述

   从程序的执行结果来看,fifo1进程打印open fifo write,fifo2进程打印了open fifo read,然后fifo1写入的数据会马上被fifo2读走。

   当我们在fifo1处按下Ctrl+c时,该进程的写端会被关闭掉,对应的在fifo2的进程的读端的read就会读到0(对端已关闭)。我们再次查看test管道文件时,发现文件内容为0,这说明FIFO是一个管道,具有管道的特性(管道中的数据只能读取一次,一旦读完就不存在了)。


   由此也证明了test文件在文件系统中只是以一个文件路径的形式而存在,在磁盘中并没有数据,所有的数据都在内核缓冲区中,当进程结束时,数据就会释放掉,但是test文件则会在文件系统中保存着,之后进程再次使用FIFO进行通信时,只需直接打开test文件就行了,然后通过test文件读写内核缓冲区的数据(可以把test文件理解为C语言中的指针,通过指针去操作这块内存),所以这才是FIFO与管道(PIPE)的真正区别。


4. 使用FIFO通信的注意事项


   1. 当一个进程调用open打开FIFO文件读取数据时会阻塞等待,直到另一进程打开FIFO文件写入数据为止。也就是说,FIFO文件必须读和写同时打开才行,如果FIFO的读写两端都已打开,那么open调用会立即返回成功,否则一个进程单独打开写或者读都会引发阻塞(上一小节已经证明了这一点)。



   2. 如果一个进程调用open函数指定O_RDWR选项来打开FIFO时不会发生阻塞,open会立即返回,不会出错,但是大多数unix实现(包括linux)对于这样的行为是未知的,这会导致进程无法正确使用管道进行通信,因为这种做法破坏了FIFO文件的I/O模型(其实就是违背了管道的通信方式)。

   换句话说,此时调用进程使用open函数返回的文件描述符读取数据时,read永远都不会读到文件末尾(至于详细原因请参考上一篇的第五小节:为何要关闭未使用的管道文件描述符)。


现在对fifo2程序做以下修改:

//以读写方式打开test
fd = open("test", O_RDWR);

程序的执行结果为:
在这里插入图片描述

   当在fifo1出按下Ctrl+c时,fifo2进程并没有终止,而是一直在阻塞。

   原因在于:fifo2进程因为是以O_RDWR(读写方式)打开test文件的,掌握着FIFO的读写两端,系统内核发现FIFO的写端还没有完全关闭,所以啥也不会做,于是fifo2就会阻塞在FIFO的读端处,等待着数据到来,但此时只有fifo2掌握着FIFO的写端,那么fifo2将会永远阻塞在读端。



   3.如果在打开FIFO文件不希望阻塞时,在调用open函数可以指定O_NONBLOCK。


5. 使用非阻塞I/O


   上一小节中说过一个进程在打开FIFO不希望阻塞时,可以在调用open函数时指定O_NONBLOCK来实现非阻塞。但是O_NONBLOCK标志在不同情况下可能会产生不同的影响,甚至会导致程序出现一些不可预料的错误。

  例如在FIFO的读端没有被打开的情况下,如果当前进程打开FIFO写入数据,那么open会调用失败,并将errno设置为ENXIO。


我们对fifo1程序做以下修改:

//以只写和非阻塞方式打开test
fd = open("test",  O_WRONLY|O_NONBLOCK);
if(fd < 0){
        //判断是否为对端没打开导致ENXIO错误
       if(errno == ENXIO){
               perror("open error");
       }
       exit(1);
}

程序执行结果:
在这里插入图片描述

  No such device or address错误的大意就是没有这样的设备或地址,出错原因在于,当你打开FIFO写入数据,但是对方没打开读端就会出现这样的错误。


6. 避免打开FIFO引发的死锁问题

   通常,进程间通信都是有两个或两个以上进程的,假设这么一种情况,当每个进程都在等待对方完成某个动作而阻塞时,这可能会产生死锁问题,下图中就展示了两个进程产生死锁的情况:
在这里插入图片描述

   在上图中的两个进程都因等待打开一个FIFO文件读取数据而阻塞。


fifo1进程

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd1 = 0;
        int fd2 = 0;
        int ret = 0;

        //创建A_FIFO
        ret = mkfifo("A_FIFO" , 0664);
        if(ret != 0){
                perror("mkfifo A_FIFO error:");
                exit(-1);
        }

        //创建B_FIFO
        ret = mkfifo("B_FIFO" , 0664);
        if(ret != 0){
                perror("mkfifo B_FIFO error:");
        }


        //以只读方式打开A_FIFO
        fd1 = open("A_FIFO",  O_RDONLY);
        if(fd1 < 0){
                perror("open A_FIFO error:");
                exit(1);
        }

        puts("open A_FIFO read");


        //以只写方式打开B_FIFO
        fd2 = open("B_FIFO" , O_WRONLY);
        if(fd2 < 0){
                perror("open B_FIFO error");
                exit(1);
        }
        puts("open B_FIFO write");
        
        close(fd1);
        close(fd2);
        return 0;
}

fifo2进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

int main(){
        int fd1 = 0;
        int fd2 = 0;

        //以读方式打开B_FIFO
        fd1 = open("B_FIFO", O_RDONLY);
        if(fd1 < 0){
                perror("open B_FIFO error:");
                exit(1);
        }
        puts("open B_FIFO read");

        //以写方式打开A_FIFO
        fd2 = open("A_FIFO" , O_WRONLY);
        if(fd2 < 0){
                perror("open A_FIFO error:");
                exit(1);
        }
        puts("open A_FIFO write");
        
        //关闭管道
        close(fd1);
        close(fd2);
        return 0;
}

程序执行结果:
在这里插入图片描述

分析死锁的原因:
   因为fifo1进程在打开A_FIFO读数据之前,fifo2进程并没有打开A_FIFO的写端,所以fifo1进程会阻塞等待在open调用处,对于fifo2进程来说也是如此,双方进程都在等待对方打开FIFO的另一端,如果不采取有效措施,双方将会一直死等下去。


   为了避免这个问题,我们可以让其中一个进程或两个线程在打开FIFO时都指定O_NONBLOCK选项以非阻塞方式解决这个问题。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82843881
今日推荐