引言:无名管道的一个重大限制是它没有名字,通信范围限定在具有血缘关系的进程间。有名管道以FIFO文件形式存在于文件系统中。这样即使与FIFO创建进程不存在血缘关系的进程,只要访问该路径,就能够通过FIFO通信。本篇笔记包括FIFO介绍、代码实例、内核实现。
FIFO指代先进先出(first in, first out),Unix中的FIFO类似于管道。它是一个单向(半双工的数据流)。不同于管道的是,每个FIFO有一个路径名与之对之关联,从而允许无亲缘关系的进程访问同一个FIFO,进行通信。FIFO也称为有名管道(named pipe)。
FIFO由mkfifo()函数创建,函数原型为:
int mkfifo(const char *pathname, mode_t mode)
mkfifo函数已隐含制定O_CREAT | O_EXCL,也就是说,它要么创建一个新的FIFO,要么返回一个EEXIST错误(所指定名字的FIFO已经存在)。如果不希望创建一个新的FIFO,那么就改为调用open而不是mkfifo。要打开一个已经存在或希望创建一个新的FIFO,应先调用mkfifo,再检查它是否返回EEXIST错误,若返回该错误则改为调用open。
1)、如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
2)、如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
总之,一旦设置了阻塞标志,调用mkfifo建立好之后,那么管道的两端读写必须分别打开,有任何一方未打开,则在调用open的时候就阻塞。对管道或者FIFO的write总是向末尾添加数据,对它们的read总是从开头返回数据,对管道或者FIFO调用lseek,返回ESPIPE错误。
如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。并且有进程写打开FIFO,且当前FIFO内没有数据,即此时管道的两端都建立好了,但是写端还没有写数据。
1)、则对于设置了阻塞标志的读操作来说,将一直阻塞(就是block住了,等待数据。它并不消耗CPU资源,这种进程的同步方式对CPU而言是非常有效率的。)
2)、对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
对于设置了阻塞标志的读操作来说,造成阻塞的原因有两种:
1)、FIFO内有数据,但有其它进程在读这些数据(对于各个读进程而言,这根有名管道是临界资源,大家得互相谦让,不能一起用。)
2)、FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。
需要注意的一点是:读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样,此时,读操作返回0。
如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。对于设置了写阻塞标志的操作:
1)、当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
2)、当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
1)、当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。
2)、当要写入的数据量小于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
基本流程:
1)、服务器创建有名管道FIFO1、FIFO2,设置FIFO1只读,FIFO2只写
2)、客户端打开有名管道FIFO1、FIFO2,设置FIFO1只写,FIFO2只读
3)、数据收发
4)、关闭读写,断开连接
服务器端程序代码:
/******************************************************
*内容:IPC通信FIFO测试,服务器端代码
*时间:2018.04.01
*问题:
******************************************************/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#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:当客户端未连接,服务器处于阻塞态:
现象2:当客户端连接后,执行状态:
因为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]