这里介绍进程的另一种通信方式——命名管道,来解决不相关进程间的通信问题。
一、什么是命名管道
命名管道也被称作FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似。
由于linux中所有的事物都可被视为文件,所以对命名管道的使用也就变得与文件操作非常的统一,也使它的使用非常方便,同时我们也可以像平常的文件名一样在命令中使用。
二、创建命名管道
我们可以使用下面两函数之一来创建一个命名管道,它们的原型如下:
#include
#include
int mkfifo(const char* filename,mode_t mode);
int mknod(const char* filename,mode_t mode | S_IFIFO,(dev_t)0);
这两个函数都能创建一个FIFO文件,注意是创建一个真实存在于文件系统中的文件,filename指定了文件名,而mode则指定了文件的读写权限。
mknod是比较老的函数,而使用mkfifo函数更加简单和规范,所以建议在可能的情况下,尽量使用mkfifo而不是mknod。
三、访问命名管道
1、打开FIFO文件
与打开其他文件一样,FIFO文件也可以使用open调用来打开。注意,mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开。
但是有两点要注意,1、就是程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。2、就是传递给open调用的是FIFO的路径名,而不是正常的文件。
打开FIFO文件通常有四种方式:
1、open(const char* path,O_RDONLY);
2、open(const char* path,O_RDONLY | O_NONBLOCK);
3、open(const char* path,O_WRONLY);
4、open(const char* path,O_WRONLY | O_NONBLOCK);
在open函数调用第二个参数中,看到一个陌生的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,加上这个选项后,表示open调用是非阻塞的,如果么有这个选项,则表示open调用是阻塞的。
open调用阻塞是什么一回事呢?很简单,对于以只读方式(O_RDONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_RDONLY),除非有一个进程以写方式打开同一个FIFO,否则它不会返回;如果open调用是非阻塞的(即第二个参数为O_RDONLY | O_NONBLOCK),则即使没有其他进程以写方式打开同一个FIFO文件,open调用将成功并立即返回。
对于以只写方式(O_WRONLY)打开的FIFO文件,如果open调用是阻塞的(即第二个参数为O_WRONLY),open调用将被阻塞,直到有一个进程以只读方式打开同一个FIFO文件为止;如果open调用是非阻塞的(即第二个参数为O_WRONLY | O_NONBLOCK),open总会立即返回,但如果没有其他进程以只读方式打开同一个FIFO文件,open调用将返回-1,并且FIFO也不会被打开。
四、使用FIFO实现进程间的通信
说了这么多,下面就用一个例子程序来说明一下,两个进程如何通过FIFO实现通信吧。这里有两个源文件,一个fifowrite.c,它在需要时创建管道,然后向管道写入数据,数据由文件Data.txt提供,大小为10M,内容全是字符‘0’。另一个源文件为fiforead.c,它从FIFO中读取数据,并把读到的数据保存到另一个文件DataFormFIFO.txt中。为了让程序更加简洁,忽略了有些函数调用是否成功的检查。
write fifo源码:
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_FILE "/tmp/fifo_test"
int main(int argc,char* argv[])
{
int readbytes = 0;
int filefd,pipefd;
char buffer[PIPE_BUF + 1] = {0};
if(access(FIFO_FILE,F_OK ) == -1)
{
if(mkfifo(FIFO_FILE,O_RDWR) == -1)
{
perror("mkfifo\n");
exit(EXIT_FAILURE);
}
}
filefd = open("./test_lock.txt",O_RDONLY);
if(filefd == -1)
{
perror("file open\n");
exit(EXIT_FAILURE);
}
pipefd = open(FIFO_FILE,O_WRONLY);
if(pipefd == -1)
{
perror("pipe open\n");
exit(EXIT_FAILURE);
}
readbytes = read(filefd,buffer,PIPE_BUF);
while(readbytes > 0)
{
buffer[PIPE_BUF + 1] = '\0';
write(pipefd,buffer,PIPE_BUF);
readbytes = read(filefd,buffer,PIPE_BUF);
}
close(filefd);
close(pipefd);
return 0;
}
read fifo源码:
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_FILE "/tmp/fifo_test"
int main(int argc,char* argv[])
{
char buffer[PIPE_BUF + 1] = {0};
int readbytes = 0;
int filefd,pipefd;
if(access(FIFO_FILE,F_OK) == -1)
{
if(mkfifo(FIFO_FILE,O_RDWR) == -1)
{
perror("mkfifo\n");
exit(EXIT_FAILURE);
}
}
filefd = open("./test_lock_tmp.txt",O_WRONLY | O_CREAT);
if(filefd == -1)
{
perror("filefd open\n");
exit(EXIT_FAILURE);
}
pipefd = open(FIFO_FILE,O_RDONLY);
{
if(pipefd == -1)
{
perror("pipefd open\n");
exit(EXIT_FAILURE);
}
}
readbytes = read(pipefd,buffer,PIPE_BUF);
while(readbytes > 0)
{
buffer[PIPE_BUF + 1] = '\0';
write(filefd,buffer,PIPE_BUF);
readbytes = read(pipefd,buffer,PIPE_BUF);
}
close(filefd);
close(pipefd);
return 0;
}
五、命名管道的安全问题
前面的例子是两个进程之间的通信问题,也就是说,一个进程向FIFO文件写数据,而另一进程则在FIFO文件中读取数据。试想这样一个问题,只使用一个FIFO文件,若有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件中读取数据时,会发生什么情况呢,会发生数据块的相互交错是很正常的?而且个人认为多个不同进程向一个FIFO读进程发送数据是很普遍的情况。
为解决这一问题,就是让写操作原子化。怎样使写操作原子化呢?答案很简单,系统规定:在一个以O_WRONLY(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等待PIPE_BUF,那么或者写入全部字节,或者一个字节也不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据绝不会交错在一起。
六、命名管道与匿名管道的对比
使用匿名管道,则通信的进程之间需要一个父子关系,通信的两个进程一定是由一个共同的祖先进程启动。但是匿名管道没有上面说到的数据交叉问题。
与匿名管道相比,命名管道相互通信的两个进程没有必然的联系,若硬要说他们具有某种联系,就只能是它们都访问同一个FIFO文件。它解决了之前在匿名管道中出现的通信的两个进程一定是由一个共同的祖先进程启动的问题。但是为了数据的安全,我们很多时候要采样阻塞的FIFO,让写操作变成原子操作。