管道通常指无名管道(PIPE)或有名管道(FIFO),但实际上套接字也都是管道。
接口PIPE和FIFO的相关接口如下表格
先来看看PIPE。既然叫管道,那么可以想象它就像一根水管,连接两个进程,一个进程要给另一个进程数据,就好像将水灌进管道一样,另一方就可以读取出来了,反过来也一样。
先来罗列pipe的特征:
(1)没有名字,因此无法使用open()
(2)只能用于亲缘进程间(如父子进程、兄弟进程、祖孙进程等)通信
(3)半双工工作方式:读写端分开
(4)写入操作不具有原子性,因此只能用于一对一的简单通信情形
(5)不能使用lseek()来定位
PIPE是一种特殊的文件,但虽然他是一种文件,却没有名字。因此,一般进程无法使用open()函数来获取它的描述符,它只能在一个进程中被创建出来,然后通过继承的方式将它的描述符传递给子进程,这就是为什么PIPE只能用于亲缘进程间通信的原因。
另外,PIPE不同于一般文件的显著之处:它有两个描述符,一个只能用来读,另一个只能用来写,这就是所谓的“半双工”通信方式。再一个显著的弱项:它对写操作不做任何保护。即使有多个进程或线程同时对PIPE进行写操作,那么这些数据很有可能会相互践踏,因此一个简单的结论是:PIPE只能用于一对一的亲缘进程通信。最后,PIPE与FIFO、socket一样,这些管道文件都不能使用lseek()来进行所谓的定位,因为它们的数据不像普通数据那样按块的方式存放在如硬盘、flash等块设备上,而更像一个看不见源头的水龙头,所以无法定位。
下面代码展示了子进程如何通过PIPE向父进程发送一段数据。
pipe.c
//////////////////////////////////////////////////////////////////
// Description: 使用无名管道pipe,实现父子进程间通信
//////////////////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int main(int argc, char **argv)
{
int fd[2];
if(pipe(fd) == -1)
{
perror("pipe()");
exit(1);
}
pid_t x = fork();
if(x == 0)
{
char *s = "hello, I am your child\n";
write(fd[1], s, strlen(s));
}
if(x > 0)
{
char buf[30];
bzero(buf, 30);
read(fd[0], buf, 30);
printf("from child: %s", buf);
}
close(fd[0]);
close(fd[1]);
return 0;
}
父进程必须先创建PIPE,然后再创建子进程,这样子进程才能继承父进程已经产生的PIPE的文件描述符。
从上述代码中可以看到,实际上父进程并没有使用PIPE的写端描述符fd[1],同理子进程也没有使用PIPE的读端描述符fd[0],所以其实它们是可以被关闭,也是应该被关闭的。
FIFO(有名管道)任何事物的优缺点都是相对的,PIPE很简单,同时也适用于场景比较单一、性能比较弱、限制条件比较多的场合;如果要在任意进程间通信,并且保证写入有原子性,那么我们可以使用FIFO。
有名管道FIFO的特征如下:
(1)有名字,存储于普通文件系统之中
(2)任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符
(3)跟普通文件一样,使用统一的read()/write()来读写
(4)跟普通文件不同:不能使用lseek()来定位
(5)写入具有原子性,支持多写者同时进行写操作而数据不会互相践踏
(6)First In First Out,最先被写入的数据,最先被读出来
下面通过一段最简单的示例代码,展示两个普通进程(Jack、Rose)如何通过FIFO互相传递信息:Jack从键盘接收一段输入并发送给Rose,Rose接收数据之后将其显示到屏幕上。
head4fifo.h
#ifndef _HEAD4FIFO_H_
#define _HEAD4FIFO_H_
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#define FIFO "/tmp/fifo4test"
#endif
Jack.c
//////////////////////////////////////////////////////////////////
// Description: 使用命名管道FIFO,实现两个进程间通信
//////////////////////////////////////////////////////////////////
#include "head4fifo.h"
int main(int argc, char **argv)
{
if(access(FIFO, F_OK))
{
mkfifo(FIFO, 0644);
}
int fifo = open(FIFO, O_WRONLY);
char msg[20];
bzero(msg, 20);
fgets(msg, 20, stdin);
int n = write(fifo, msg, strlen(msg));
printf("%d bytes have been sended.\n", n);
return 0;
}
Rose.c
//////////////////////////////////////////////////////////////////
// Description: 使用命名管道FIFO,实现两个进程间通信
//////////////////////////////////////////////////////////////////
#include "head4fifo.h"
int main(int argc, char **argv)
{
if(access(FIFO, F_OK))
{
mkfifo(FIFO, 0644);
}
int fifo = open(FIFO, O_RDONLY);
char msg[20];
bzero(msg, 20);
read(fifo, msg, 20);
printf("from FIFO: %s", msg);
return 0;
}
注意以下几点:
(1)代码第5行中的函数access()通过指定参数F_OK来判断一个文件是否存在,另外还可以通过别的参数来判断文件是否可读、可写、可执行等。
(2)当刚开始运行Jack而尚未运行Rose,或是刚开始运行Rose尚未运行Jack时,open函数会被阻塞,因为管道文件(包括PIPE、FIFO、SOCKET)不可以在只有读端或只有写端的情况下被打开
(3)当Jack已经打开但还没写入数据之前,Rose将在read()上阻塞睡眠,直到Jack写入数据完毕为止。因为默认状态下是以阻塞方式读取数据的,可以使用fcntl()来使得FIFO变成非阻塞模式。
不仅打开管道会有可能发生阻塞,在对管道进行读/写操作时也有可能发生阻塞,可以参考下面的表格。
所谓写者:持有文件可写权限描述符的进程
所谓读者:持有文件可读权限描述符的进程
FIFO与PIPE还有一个最大的不同点在于:FIFO具有一种所谓写入原子性的特征,这种特征使得我们可以同时对FIFO进行写操作而不怕数据遭受破坏,一个典型的应用时linux的日志系统。
系统的日志信息被统一安排存放在/var/log下,这些日志文件都是一些普通的文本文件。普通文件可以被一个或多个进程重读多次打开,每次打开都有一个独立的位置偏移量,如果多个进程或线程同时写文件,那么除非它们之间能相互协调好,否则必然导致混乱。
事实上:需要写日志的进程根本不可能“协调好,系统日志实际上就相当于一个公共厕所,系统中阿猫阿狗都可以进去拉撒一通,由于写日志的进程是毫无关联的,因此常用的互斥手段(如互斥锁、信号量等)是无法起作用的,就好像无法试图通过交通规则来杜绝有人乱闯红灯一样,因为总有人可以故意无视规则,肆意践踏规则。
如何使得毫不相干的不同进程的日志信息都能完整地输送到日志文件中而不相互破坏,是一个必须解决的问题。一个简单高效的方案是:使用FIFO来接收各个不相干进程的日志信息,然后让一个进程专门将FIFO中的数据写到相应的日志文件中。这样做的好处是:任何进程无需对日志信息的互斥编写出任何额外的代码,只管向FIFO里面写入即可。后台默默耕耘的日志系统服务例程会将这些信息一一地拿出来再写入日志文件,FIFO的写入原子性保证了数据的完整无缺。
总结本文详细介绍了匿名管道和有名管道这两个用于进程间通信的方式,并总结了他们的特点和使用场景;也通过示例演示了两个进程如何通过管道进行通信,对管道这种通信方式有了更深刻的理解。