命名管道

命名管道(FIFO)

简介

  • 管道没有名字,并且随着进程存在消失.所以我们没办法通过管道使两个无亲缘关系的进程通信.
  • FIFO指的是first in first out.同管道一样,它也是一个半双工数据流.不同的是,每一个FIFO有一个路径名与之关联.
    从这个特点上我们可以看到,我们可以通过一个路径名使没有亲缘关系的进程通过FIFO来进行通信.
  • 我们通过mkfifo创建FIFO,下面我们给出mkfifo的函数原型
    #include<sys/stat.h>
    #include<sys/types.h>

    int mkfifo(const char *pathname, mode_t mode);
                //成功则返回0,失败则返回-1
                /*
                    mode常量值:
                    S_IRUSR 用户(属主)读
                    S_IWUSR 用户(属主)写
                    S_IRGRP (属)组成员读
                    S_IWGRP (属)组成员写
                    S_IROTH 其他用户读
                    S_IWOTH 其他用户写
                */
  • FIFO通过open或者fopen等标准IO打开.同时FIFO不能打开既读又写,因为它是半双工的.
  • FIFO的write总是往末尾添加数据,read则从开头返回数据.不能使用lseek.

例子

要求:进程a读取路径名,进程b获得路径名并读取文件内容并发送给进程a

//进程a
#include "fifo_conf.h"
#include <cstdio>
#include <errno.h>
#include <limits.h>
#include<sys/stat.h>
#include<sys/types.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

void server(int,int);

int main(int argc, char const *argv[]) {
    int readfd,writefd;

    if((mkfifo(FIFO_S, FILE_MODE) < 0) && (errno != EEXIST)){
        fprintf(stderr, "Can't make a new FIFO at %s\n",FIFO_S);
    }

    if((mkfifo(FIFO_C, FILE_MODE)) < 0) && (errno != EEXIST){
        unlink(FIFO_S);
        fprintf(stderr, "Can't make a new client FIFO at %s\n",FIFO_C);
    }

    readfd = open(FIFO_S, O_RDONLY, 0);
    writefd = open(FIFO_C, O_WRONLY, 0)

    server(readfd, writefd);
    return 0;
}

void server(int readfd, int writefd) {
    char pathname[PATH_MAX+1];
    char buf[PIPE_BUF];

    printf("Please enter the path: \n");
    scanf("%s",pathname);

    write(writefd, pathname, strlen(pathname));

    while((read(readfd, buf, PIPE_BUF)) != 0)
        printf("%s\n", buf);

    unlink(FIFO_S);
}

//进程b
#include "fifo_conf.h"
#include <cstdio>
#include <errno.h>
#include <limits.h>
#include<sys/stat.h>
#include<sys/types.h>

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

void client(int, int);

int main(int argc, char const *argv[]) {
    int readfd,writefd;

    if((mkfifo(FIFO_C, FILE_MODE)) < 0 && errno != EEXIST){
        fprintf(stderr, "Can't make a new FIFO at %s\n", FIFO_C);
    }

    readfd = open(FIFO_C, O_RDONLY, 0);

    if((writefd = open(FIFO_S, O_WRONLY, 0)) < 0){
        unlink(FIFO_C);
        fprintf(stderr, "Can't open the server FIFO at %s\n", FIFO_S);
    }

    client(readfd, writefd);
    return 0;
}

void client(int readfd, int writefd){
    int fd,n;
    char buf[PIPE_BUF];
    char pathname[PATH_MAX+1];

    if((n = read(readfd, pathname, PATH_MAX)) == 0){
        fprintf(stderr, "Can't read path from FIFO at %s\n", FIFO_C);
        unlink(FIFO_C);
        exit(0);
    }

    pathname[n] = '\0';

    if((fd = open(pathname, O_RDONLY)) < 0){
        fprintf(stderr, "Can't open file at %s\n", pathname);
        unlink(FIFO_C);
        exit(0);
    }

    while((n = read(fd, buf, PIPE_BUF)) != 0){
        write(writefd, buf, n);
    }

    unlink(FIFO_C);
}

例子解析

  • 可以看到的是,FIFO和pipe的区别主要集中在两个方面.
    • 创建方式的不同:pipe通过pipe()创建并返回两个描述符.而FIFO通过mkfifo创建并返回该描述符
    • 参数不同:pipe传入的是用来存放描述符的数组,FIFO传入的是路径以及文件权限模式.
  • 我们在调用mkfifo的时候,如果该路径已经存在,那么则会有一个EEXIST错误.
  • 可以说FIFO是带有一些文件性质的,因为我们同样也需要指定打开方式(读or写).
  • 可以看到,我们最后在Client中调用unlink函数删除两个FIFO.

FIFO与管道相关内容

  • 内核为管道和FIFO维护了一个访问计数器,它的值是访问同一个管道或者FIFO的打开着的描述符数量,听起来很像GC中的引用计数器.
    即使我们调用了unlink删除了这个FIFO也不会对已经打开的描述符造成影响.通过查阅资料印证了我的猜测,即FIFO只是借助文件系统,起到
    一个索引的作用.而真实的数据其实仍旧在内核中.所以我们删除了该文件但不会对数据造成影响.

  • FIFO中write的原子性.因为FIFO的特点,所以它也经常被多个进程同时使用.为了避免造成这样的情况,就需要在write层面提供原子性操作
    以避免乱序的问题.在FIFO中,只要写入的buf长度是小于PIPE_BUF的,那么操作系统就可以保证这是一个原子行为.

  • FIFO的阻塞问题.open函数在打开FIFO管道的时候在某些情况下是阻塞的.首先我们来看一段代码

    int fd;

    if((mkfifo(FIFO_C, FILE_MODE)) < 0 && errno != EEXIST){
        fprintf(stderr, "Can't make a new FIFO at %s\n", FIFO_C);
    }

    printf("TEST TOP\n");
    fd = open(FIFO_C, O_WRONLY, 0);
    printf("TEST BOTTOM\n");

在本例中,"TEST BOTTOM"不会被输出.因为open函数一直被阻塞住没有返回.
那么FIFO在什么情况下会阻塞呢?
- 首先我们要明确概念,阻塞是一个双向的行为,进程a阻塞的目的是等待进程b的某些行为触发的信号.
- 在没有设置阻塞位(下文有提及)的时候,FIFO必须是双向打开的,也就是说进程a如果以读的形式打开FIFO,此时open操作会被阻塞住.直到进程b以写的形式打开FIFO,此时进程a会被唤醒然后
继续执行下去.再强调一次,阻塞的FIFO必须双向打开,否则就会造成死锁.
- 如果一端关闭了管道,而另一端向管道中写或者读都会出现问题.read返回0(文件结束符),write给线程产生SIGPIPE信号(默认行为是终止进程).

  • 阻塞给我们的程序带来了许多不便,此时我们应该怎么办呢?
    • 首先我们可以在open时通过加入O_NONBLOCK标志位(open(file, O_RDONLY | O_NONBLOCK))来设定操作均为非阻塞行为.
    • 非阻塞模式的FIFO行为模式发生了改变.当我们以只读方式打开FIFO时,描述符均会成功返回.当我们以只写方式打开FIFO时,如果FIFO没有读端则会返回一个ENXIO错误而不会阻塞.当我们read一个空FIFO时,
      如果此时FIFO有写端则会返回EAGAIN,如果没有则返回0.当我们write一个FIFO时,没有读端则产生一个SIGPIPE信号.
    • O_NONBLOCK不影响write的原子性.

字节流

FIFO和管道类似于TCP,都是使用的标准的Stream IO模式.也就是说它们并没有"包"这个概念,进程A写入100字节和进程A写入50字节,进程B写入50字节在表现形式上来看是相同的.
如果我们需要边界这个概念的话,那么需要在应用程序层面是手动添加.常见的方法如下:

  • 特殊分割字符: 例如 /r/n
  • 显式长度: 每个记录前冠以它的长度
posted @ 2017-03-10 18:26  XLLL  阅读(810)  评论(0编辑  收藏  举报