分类: LINUX
2012-02-10 21:00:58
++++++APUE读书笔记-15进程内部通信-05FIFOs++++++
5、FIFOs
================================================
FIFOs有时被称作有名管道。Pipes只能用在祖先进程创建的pipe之后具有关系的进程之间的通信(不考虑后面将要讲述的基于流的挂载的管道),而FIFOs可以用在无关进程之间的通信。
前面我们已经看到过,管道其实是一种文件,其stat结构中的st_mode成员会标志该文件是管道,我们通过S_ISFIFO宏来对它进行测试。
创建一个FIFO和创建一个文件类似,其实,其路径名是文件系统中存在的。
#include
int mkfifo(const char *pathname, mode_t mode);
返回:如果成功返回0,如果错误返回1。
参数mode和open函数的mode参数一样。
创建了FIFO之后,我们可以使用open函数打开它,一般的文件I/O函数(例如close,read,write,unlink等)对FIFO也是适用的。
应用程序可以通过mknod函数来创建管道,但是POSIX.1中没有包含mknod函数,所以POSIX.1中引入了mkfifo函数,现在mknod被包含在XSI扩展中,在大多数系统上面,mkfifo调用mknod来创建管道。
当打开一个FIFO的时候,非阻塞标记会(O_NONBLOCK)有一些影响:
* 一般来说(O_NONBLOCK没有被指定),使用只读的open会导致阻塞,直到其他的进程打开FIFO用来写入;类似,使用只写的open也会导致阻塞,直到其他的进程打开FIFO用来读取。
* 如果一个O_NONBLOCK被指定了,那么只读的open,将会立即返回。但是如果没有进程对FIFO打开进行读取的话,只写的open返回1并且设置errno为ENXIO
和pipe一样,如果我们写一个没有进程读的管道,那么会产生SIGPIPE信号,如果最后一个写FIFO的进程关闭FIFO那么会为读FIFO的进程产生一个文件结束符号。
多个进程写一个给定的FIFO是非常常见的。这样我们就不得不考虑一下写操作的原子性,防止多个进程的写相互干扰。和pipes类似,常量PIPE_BUF指定可以原子写的最大的数据量。
FIFOs有两个用途:
a)被shell命令使用将数据从一个管道线传递到另外一个管道线,同时不创建中间文件。
b)做为一个客户服务程序之间的交互点,用来在客户程序和服务程序之间传输数据。
我们会使用例子来进行描述。
使用管道重定向输出流的例子
FIFOs可以被用来重定向一系列shell命令的输出流。这个会防止将数据写入到一个中间文件中(类似pipe)。pipes只能用于在进程之间进行线性的连接,而FIFO有它的名称,所以可以用来进行非线性的连接。
假设一个过程,需要处理一个过滤好的输入流两次,如图:
+--------------+
----> | prog3 |
--/ +--------------+
+------------+ --/
input file--->| prog1 |
+------------+ --\ +--------------+
---\ | prog2 |
---> +--------------+
通过使用FIFO和UNIX的程序tee,我们可以不经过中间文件完成这个过程。(tee程序会将它的标准输入拷贝到它的标准输出以及命令行上指定的一个文件中)。
mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
我们创建一个FIFO然后在后台启动prog3,从FIFO中读取。我们之后启动prog1然后使用tee将它的标准输入发送到FIFO和prog2中。如下图所示:
+-------+ +------+
----> | FIFO |---->|prog3 |
--/ +-------+ +------+
+--------+ +------------+ --/
input file->|prog1 |---->| tee |
+--------+ +------------+ --\ +-------+
---\ | prog2 |
---> +-------+
一个客户服务使用FIFO进行通信的例子
另外一个使用FIFOs的例子就是在客户和服务器模型之间进行通信。如果我们有一个连接了多个客户程序的服务,每个客户程序都会把自己的请求写入到服务创建的一个公共的FIFO中(这里的"well-known"表示我们使用的FIFO的路径名称是所有的client都知道了的)下面的图就表示了这个情况。
+---------------+
| server |
+-------^-------+
|
read requests
+-------+-------+
| well-knownFIFO|
+--^----------^-+
/ \
write requests write requests
/ \
/ \
+------------+ +-----------+
| client | ...... | client |
+------------+ +-----------+
因为对于一个FIFO有多个写入者,所以客户发送给server的请求需要小于PIPE_BUF字节,这样会客户之间的写操作互相干扰。
使用这个FIFOs模型的问题在于服务端如何将请求发送回每个客户程序。这个时候单一的FIFO就无法使用了,因为每个客户都有自己的请求相应,但是每个客户却无法知道什么时候从FIFO中读取到它自己的请求相应。有一个解决的方法就是,每个客户在其请求中同时发送它自己的进程ID,服务会为每个客户创建单独的FIFO,创建的路径名称基于客户的进程ID。例如,服务可以创建一个名称为"/tmp/serv1.XXXXX"的FIFO,这里XXXXX就是客户程序的进程ID。下图描述了这个结构:
+---------------+
| server |
/ +-------^-------+ \
write replies | write replies
/ read requests \
+--------------v-+ +-------+-------+ +-v---------------+
|client-FIFO | | well-knownFIFO| | client-FIFO |
+---\------------+ +--^----------^-+ +-------------/---+
\ / \ /
\ write requests write requests /
read replies / \ read replies
\ / \ /
+v-----------+ +----------v+
| client | ...... | client |
+------------+ +-----------+
这样的方式会正确的工作,但是有一个缺点就是服务无法知道如何判断一个客户是否终止。这样会导致客户端的FIFOs留存在了文件系统当中。服务必须捕获SIGPIPE信号,因为可能客户发送请求之后,在读取相应之前就终止了,这样留下的相应的客户端FIFO就之后一个写进程(server)却没有读进程了(前面说过这个时候会产生SIGPIPE)。我们后面在讨论到基于流的管道的时候,会讨论解决这个问题的比较不错的算法。
对于前面的结构,如果服务进程打开well-known的FIFO的时候使用的是read-only方式(也就是只从其中读取数据),那么每次当客户进程数目从1变成0的时候,服务进程都会从FIFO中读取到一个文件结尾符号(因为前面说过,最后一个写进程关闭管道的时候会在管道中写入一个文件结束符号)。为了防止这样的情况发生,服务进程采用了这样的方法:打开它的well-known FIFO的时候使用readwrite模式(本书本章中的一个练习提到过)。
参考: