linux进程通信———有名管道FIFO

linux进程通信———有名管道FIFO

引言:无名管道的一个重大限制是它没有名字,通信范围限定在具有血缘关系的进程间。有名管道以FIFO文件形式存在于文件系统中。这样即使与FIFO创建进程不存在血缘关系的进程,只要访问该路径,就能够通过FIFO通信。本篇笔记包括FIFO介绍、代码实例、内核实现。



一、FIFO简介

1.1、“有名”管道:

  FIFO指代先进先出(first in, first out),Unix中的FIFO类似于管道。它是一个单向(半双工的数据流)。不同于管道的是,每个FIFO有一个路径名与之对之关联,从而允许无亲缘关系的进程访问同一个FIFO,进行通信。FIFO也称为有名管道(named pipe)。

这里写图片描述
图1、pipe与fifo对比,详情参考man 7 pipe

1.2、创建:

  FIFO由mkfifo()函数创建,函数原型为:

int mkfifo(const char *pathname, mode_t mode)

  • const char *pathname:是一个普通的unix路径名,它是该FIFO的名字。
  • **mode:**mode参数指定FIFO权限位,它是S_IRUSER(属主读)、S_IWUSR(属主写)、S_IRGRP(组成员读)、S_IWGRP(组成员写)、S_IROTH(其他用户读)、S_IWOTH(其他用户写)这六个常值按位或组成的。

这里写图片描述
图2、mkfifo函数man手册

  mkfifo函数已隐含制定O_CREAT | O_EXCL,也就是说,它要么创建一个新的FIFO,要么返回一个EEXIST错误(所指定名字的FIFO已经存在)。如果不希望创建一个新的FIFO,那么就改为调用open而不是mkfifo。要打开一个已经存在或希望创建一个新的FIFO,应先调用mkfifo,再检查它是否返回EEXIST错误,若返回该错误则改为调用open。

1.3、FIFO的打开规则:

1)、如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
2)、如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

总之,一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞。对管道或者FIFO的write总是向末尾添加数据,对它们的read总是从开头返回数据,对管道或者FIFO调用lseek,返回ESPIPE错误。

这里写图片描述
图3、管道内数据流动

1.4、FIFO中读取数据:

  如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。并且有进程写打开FIFO,且当前FIFO内没有数据,即此时管道的两端都建立好了,但是写端还没有写数据

1)、则对于设置了阻塞标志的读操作来说,将一直阻塞(就是block住了,等待数据。它并不消耗CPU资源,这种进程的同步方式对CPU而言是非常有效率的。)
2)、对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。

  对于设置了阻塞标志的读操作来说,造成阻塞的原因有两种

1)、FIFO内有数据,但有其它进程在读这些数据(对于各个读进程而言,这根有名管道是临界资源,大家得互相谦让,不能一起用。)
2)、FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

需要注意的一点是:读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样,此时,读操作返回0。

这里写图片描述
图4、pipe 容量与缓存大小,详情见man 7 pipe

1.5、FIFO中写入数据:

  如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。对于设置了写阻塞标志的操作:

1)、当要写入的数据量不大于PIPE_BUF时linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
2)、当要写入的数据量大于PIPE_BUF时linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。

对于没有设置阻塞标志的写操作:

1)、当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
2)、当要写入的数据量小于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;


二、代码实例:

2.1、无亲缘关系的服务器与客户端通信:

这里写图片描述
图5、无亲缘的客户端与服务器双向通信

基本流程:

1)、服务器创建有名管道FIFO1、FIFO2,设置FIFO1只读,FIFO2只写
2)、客户端打开有名管道FIFO1、FIFO2,设置FIFO1只写,FIFO2只读
3)、数据收发
4)、关闭读写,断开连接

服务器端程序代码:

/******************************************************
*内容:IPC通信FIFO测试,服务器端代码
*时间:2018.04.01
*问题:
******************************************************/

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

#define FIFO1 "/home/db/share/fifo1"
#define FIFO2 "/home/db/share/fifo2"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IRUSR)
#define MAXLINE 1024

int main(int argc, char* argv[])  
{  

    int server_read_fd = -1;
    int server_write_fd = -1;
    char server_buf[MAXLINE] = {0};



    //第一步、用户输入合法性检测
    if (argc != 2) 
    {
        fprintf(stderr, "usage : %s\n", argv[0]);
        exit(1);
    }

    //第二步、创建有名管道FIFO1、FIFO2
    if((mkfifo(FIFO1, FILE_MODE)<0) && (errno != EEXIST))
    {
        printf("can't creat %s", FIFO1);
    }

    if((mkfifo(FIFO2, FILE_MODE)<0) && (errno != EEXIST))
    {
        printf("can't creat %s", FIFO2);
    }


    //第三步、open连接FIFO1的读、FIFO2的写端口
    server_read_fd = open(FIFO1, O_RDONLY, 0);
    if(-1 != server_read_fd)
    {
         printf("Process %d opening FIFO1 O_RDONLY\n", getpid()); 
    }
    server_write_fd = open(FIFO2, O_WRONLY, 0);
    if(-1 != server_read_fd)
    {
         printf("Process %d opening FIFO2 O_WDONLY\n", getpid()); 
    }


    //第四步、服务器等待接收客户端的数据
    sleep(3);
    if (server_read_fd != -1)  
    {  
        memset(server_buf, 0 , sizeof(server_buf));
        read(server_read_fd, server_buf, MAXLINE);
        printf("server receive message: %s\n", server_buf);
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  


    //第五步、服务器发送argv[1]内容(没有应用协议,不做解析处理,):
    sleep(3);
    if (server_write_fd != -1)  
    {  
        write(server_write_fd, argv[1], strlen(argv[1]));
        printf("server send message: %s\n", argv[1]);
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  

    //第六步:进程退出
    printf("Process %d finish\n", getpid());  
    exit(EXIT_SUCCESS);  
}

客户端程序代码:

/******************************************************
*内容:IPC通信FIFO测试,客户端代码
*时间:2018.04.01
*问题:
******************************************************/

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


#define FIFO1 "/home/db/share/fifo1"
#define FIFO2 "/home/db/share/fifo2"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IRUSR)
#define CLI_MAXLINE 1024



int main(int argc, char* argv[])  
{  

    int client_read_fd = -1;
    int client_write_fd = -1;
    char client_buf[CLI_MAXLINE] = {0};



    //第一步、用户输入合法性检测
    if (argc != 2) 
    {
        fprintf(stderr, "usage : %s\n", argv[0]);
        exit(1);
    }

    //第二步、open连接FIFO1的写、FIFO2的读端口
    client_write_fd = open(FIFO1, O_WRONLY, 0);
    if(-1 != client_write_fd)
    {
         printf("Process %d opening FIFO1 O_WRONLY\n", getpid()); 
    }
    client_read_fd = open(FIFO2, O_RDONLY, 0);
    if(-1 != client_read_fd)
    {
         printf("Process %d opening FIFO2 O_RDONLY\n", getpid()); 
    }


    //第三步、客户端向服务器发送数据
    if (client_write_fd != -1)  
    {  
        write(client_write_fd, argv[1], strlen(argv[1]));
        printf("client send message: %s\n", argv[1]);
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  


    //第四步、接收服务器回射消息
     if (client_read_fd != -1)  
    {  
        memset(client_buf, 0 , sizeof(client_buf));
        read(client_read_fd, client_buf, CLI_MAXLINE);
        printf("client receive message: %s\n", client_buf);
    }  
    else  
    {  
        exit(EXIT_FAILURE);  
    }  


    //第五步:进程退出,关闭链接
    close(client_read_fd);
    close(client_write_fd);

    unlink(FIFO1);
    unlink(FIFO2);
    printf("Process %d finish\n", getpid());  
    exit(EXIT_SUCCESS);  
}

现象1:当客户端未连接,服务器处于阻塞态:

这里写图片描述
图6、无客户请求状态

现象2:当客户端连接后,执行状态:

这里写图片描述
图7、正常执行状态


三、FIFO命名管道的内核实现:

  因为pipe只能用在两个有血缘关系的进程上,例如父子进程;如果要在两个没有关系的进程上利用管道通信时,这时pipe就派不上用场了。我们如何让两个不相干的进程找到带有pipe属性的inode呢?自然想到利用磁盘文件。
  linux下两个进程访问同一个文件时,虽然各自的file是不一样的,但是都指向同一个Inode节点,所以将pipe和磁盘文件结合,就产生了命名管道。

//read_fifo_fpos
struct file_operations read_fifo_fops = {
    .llseek     = no_llseek,
    .read       = pipe_read,
    .readv      = pipe_readv,
    .write      = bad_pipe_w,
    .poll       = fifo_poll,
    .ioctl      = pipe_ioctl,
    .open       = pipe_read_open,
    .release    = pipe_read_release,
    .fasync     = pipe_read_fasync,
};
// read_pipe_fops
struct file_operations read_pipe_fops = {
    .llseek     = no_llseek,
    .read       = pipe_read,
    .readv      = pipe_readv,
    .write      = bad_pipe_w,
    .poll       = pipe_poll,
    .ioctl      = pipe_ioctl,
    .open       = pipe_read_open,
    .release    = pipe_read_release,
    .fasync     = pipe_read_fasync,
};

  可以看出来,二者操作函数一样,说明对FIFO的读写操作也是对管道缓冲区进行读写,故fifo创建的文件,只是让读写进程找到相同的inode, 进而操作相同的pipe缓冲区。


参考资料:
1、unix 网络编程卷II
2、Linux有名管道FIFO)
3、从内核代码聊聊pipe的实现

纠错与建议
邮箱:[email protected]


猜你喜欢

转载自blog.csdn.net/xd_hebuters/article/details/79778673