Linux/Unix系统IPC是各种进程间通信方式的统称,但是其中极少能在所有Linux/Unix系统实现中进行移植。随着POSIX和Open Group(X/Open)标准化的推进呵护影响的扩大,情况虽已得到改善,但差别仍然存在。一般来说,Linux/Unix常见的进程间通信方式有:管道、消息队列、信号、信号量、共享内存、套接字等。博主将在《进程间通信方式总结》系列博文中和大家一起探讨学习进程间通信的方式,并对其进行总结,让我们共同度过这段学习的美好时光。这里我们就以其中一种方式管道展开探讨,管道是Linux/Unix系统IPC的最古老形式,并且所有Linux/Unix系统都提供此种通信机制。管道又分为无名管道和有名管道,无名管道只能用于有亲缘关系的进程间通信,有名管道则可以用于非亲缘进程间的通信。本篇就以管道中的有名管道(又称命名管道)为例,希望对你有所帮助。
命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于Linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用它。
int mkfifo(const char *path, int mode);
与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道需要使用open将其打开。但是有两点要注意:
①程序不能以O_RDWR模式打开FIFO文件进行读写操作,如果你非要这么做,其行为未明确定义,因为如果一个管道以读/写方式打开,进程就会读回自己的输出,我们通常使用FIFO只是为了单向的数据传递。
②传递给open调用的是FIFO的路径名,而不是正常的文件。
在open函数的调用的第二个参数中,你看到一个陌生的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果没有这个选项,则表示open调用是阻塞的。
open(const char *path, int mode);
open调用的阻塞是怎么一回事呢?很简单,对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;如果open调用是非阻塞的的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件(前提是FIFO文件存在),open调用将成功并立即返回。
对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。哈哈,是不是很绕啊,没关系,举个栗子你就明白了,下面我们来看个简单的栗子吧:
fifowrite.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <limits.h> //PIPE_BUF
int main()
{
int fifo_fd = -1;
int data_fd = -1;
const char *filename = "/home/tangf/.vimrc";
const char *fifoname = "./fifo";
char buffer[PIPE_BUF + 1] = {0};
//检查调用进程是否可以对指定的文件执行某种操作(成功返回0,错误返回-1)
//R_OK 测试读许可权
//W_OK 测试写许可权
//X_OK 测试执行许可权
//F_OK 测试文件是否存在
if (-1 == access(fifoname, F_OK))
{
//创建fifoname管道文件,权限为0666
int ret = mkfifo(fifoname, 0666);
//创建失败处理
if (-1 == ret)
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
//以阻塞方式打开fifoname管道文件
if (-1 == (fifo_fd = open(fifoname, O_WRONLY/* | O_NONBLOCK*/)))
{
perror("open fifo_fd");
exit(EXIT_FAILURE);
}
//打开读文件
if (-1 == (data_fd = open(filename, O_RDONLY)))
{
close(fifo_fd);
perror("open data_fd:");
exit(EXIT_FAILURE);
}
int read_len = read(data_fd, buffer, PIPE_BUF);
buffer[read_len] = '\0';
while (read_len > 0)
{
//将读取的数据写入管道
int ret = write(fifo_fd, buffer, read_len);
//写入失败,做异常处理
if (-1 == ret)
{
close(data_fd);//关闭文件描述符
close(fifo_fd);
perror("write");//打印错误信息
exit(EXIT_FAILURE);//退出程序
}
//从文件中读取数据
read_len = read(data_fd, buffer, PIPE_BUF);
buffer[read_len] = '\0';
}
//关闭文件描述符
close(data_fd);
close(fifo_fd);
exit(EXIT_SUCCESS);
}
fiforead.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <string.h>
#include<limits.h> //PIPE_BUF
int main()
{
int fifo_fd = -1;
int data_fd = -1;
const char *filename = "./vimrc";
const char *fifoname = "./fifo";
char buffer[PIPE_BUF + 1] = {0};
//检查调用进程是否可以对指定的文件执行某种操作(成功返回0,错误返回-1)
//R_OK 测试读许可权
//W_OK 测试写许可权
//X_OK 测试执行许可权
//F_OK 测试文件是否存在
if (-1 == access(fifoname, F_OK))
{
//创建fifoname管道文件,权限为0666
int ret = mkfifo(fifoname, 0666);
//创建失败处理
if (-1 == ret)
{
perror("mkfifo");
exit(EXIT_FAILURE);
}
}
}
//以非阻塞方式打开fifoname管道文件
if (-1 == (fifo_fd = open(fifoname,O_RDONLY | O_NONBLOCK)))
{
perror("open fif_fd");
exit(EXIT_FAILURE);
}
//以只写方式打开存储文件,不存在则创建
if (-1 == (data_fd = open(filename,O_WRONLY | O_CREAT, 0666)))
{
close(fifo_fd);
perror("open data_fd");
exit(EXIT_FAILURE);
}
//从管道中读取数据
int read_len = read(fifo_fd, buffer,PIPE_BUF);
buffer[read_len] = '\0';
while (read_len > 0)
{
//将读取的数据写入文件
int ret = write(data_fd, buffer,read_len);
if (-1 == ret)
{
close(data_fd);//关闭描述符
close(fifo_fd);
perror("write");//打印错误信息
exit(EXIT_FAILURE);//退出程序
}
//从管道中读取数据
read_len = read(fifo_fd, buffer,PIPE_BUF);
buffer[read_len] = '\0';
}
//关闭文件描述符
close(data_fd);
close(fifo_fd);
exit(EXIT_SUCCESS);
}
程序运行结果:
上面的栗子是两个进程之间的通信问题,一个进程向FIFO文件写数据,而另一个进程则从FIFO文件中读取数据。试想这样一个问题,只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件中写入数据,而且只有一个读FIFO进程在同一个FIFO文件中读取数据时,会发生怎么样的情况呢,会发生数据块的相互交错么?而且个人认为多个不同进程向一个FIFO读进程发送数据是很普通的情况。
为了解决这一问题,就是让写操作原子化。怎样才能使写操作原子化呢?答案很简单,系统规定:在一个以O_WRONLY(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写入请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据块不会交错在一起。
我们在《进程间通信方式总结——管道(一)》中已经对无名管道(又称匿名管道)的使用进行的简单的总结,这里我们又对有名管道(又称命名管道)的使用进行的简单的总结,下面就让我们一起简单的聊聊它们之间的区别吧!
使用匿名管道,则通信的进程之间需要一个父子关系,通信的两个进程一定是由一个共同的祖先进程启动。但是匿名管道没有上面说到的数据交叉的问题。
与使用匿名管道相比,我们可以看到write和read这两个进程是没有什么必然的联系的,如果硬要说他们具有某种联系,就只能说是它们都访问同一个FIFO文件。它解决了之前在匿名管道中出现的通信的两个进程一定是由一个共同的祖先进程启动的问题。但是为了数据的安全,我们很多时候要采用阻塞的FIFO,让写操作变成原子操作。
关于管道的学习我们就到此结束了,相信大家都有所收获,希望小伙伴们都已经理解并掌握了管道的常用方法。如果你觉得对进程间通信的方式不胜了解,还有些许疑惑,请关注博主《进程间通信方式总结》系列博文,相信你在那里能找到答案。