命名管道
命名管道(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
- 显式长度: 每个记录前冠以它的长度