命名管道(FIFO)
简介
- 管道没有名字,并且随着进程存在消失.所以我们没办法通过管道使两个无亲缘关系的进程通信.
- FIFO指的是first in first out.同管道一样,它也是一个半双工数据流.不同的是,每一个FIFO有一个路径名与之关联. 从这个特点上我们可以看到,我们可以通过一个路径名使没有亲缘关系的进程通过FIFO来进行通信.
- 我们通过mkfifo创建FIFO,下面我们给出mkfifo的函数原型
#include#include 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#include #include #include #include #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 #include #include #include #include #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
- 显式长度: 每个记录前冠以它的长度