基于STREAMS的管道(简称STREAMS管道,STREAMS pipe)是一个双向(全双工)管道。单个STREAMS管道就能向父、子进程提供双向的数据流。
将http://www.cnblogs.com/nufangrensheng/p/3560130.html中图15-1中的单向箭头全部换成双向箭头,即为观察STREAMS管道的两种方式。
图17-1 观察STREAMS管道的两种方式
如果从内部观察STREAMS管道(图17-2),可以看到它简单得只包含两个流首,每个流首的写队列(WQ)指向另一个流首的读队列(RQ),写入管道一端的数据被放入另一端的读队列的消息中。
图17-2 STREAMS管道的内部结构
因为STREAMS管道是一个流,所以可将STREAMS模块压入到该管道的任一端(图17-3)。但是,如果我们在一端压入了一个模块,那么并不能在另一端弹出该模块。如果想要删除它,则必须从原压入端删除。
图17-3 带模块的STREAMS管道内部结构
实例
下面用一个STREAMS管道再次实现程序清单15-9(http://www.cnblogs.com/nufangrensheng/p/3561379.html)中的协同进程实例。程序清单17-1是新的main函数。add2协同进程与程序清单15-8(http://www.cnblogs.com/nufangrensheng/p/3561379.html)中的相同。本程序调用了创建单个STREAMS管道的新函数s_pipe(见下个实例)。
程序清单17-1 用STREAMS管道驱动add2过滤进程的程序
#include "apue.h" static void sig_pipe(int); /* our signal handler */ int main(void) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if(signal(SIGPIPE, sig_pipe) == SIG_ERR) err_sys("signal error"); if(s_pipe(fd) < 0) /* need only a single stream pipe */ err_sys("pipe error"); if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid > 0) { close(fd[1]); /* parent */ while(fgets(line, MAXLINE, stdin) != NULL) { n = strlen(line); if(write(fd[0], line, n) != n) err_sys("write error to pipe"); if((n = read(fd[0], line, MAXLINE)) < 0) err_sys("read error from pipe"); if(n == 0) { err_msg("child closed pipe"); break; } line[n] = 0; /* null terminate */ if(fputs(line, stdout) == EOF) err_sys("fputs error"); } if(ferror(stdin)) err_sys("fgets error on stdin"); exit(0); } else { close(fd[0]); if(fd[1] != STDIN_FILENO && dup2(fd[1], STDIN_FILENO) != STDIN_FILENO) err_sys("dup2 error to stdin"); if(fd[1] != STDOUT_FILENO && dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO) err_sys("dup2 error to stdout"); if(execl("./add2", "add2", (char *)0) < 0) err_sys("execl error"); } exit(0); } static void sig_pipe(int signo) { printf("SIGPIPE caught "); exit(1); }
父进程只使用fd[0],子进程只是用fd[1]。因为STREAMS管道的每一端都是全双工的,所以父进程读、写fd[0],而子进程将fd[1]复制到标准输入和标准输出。图17-4显示了由此构成的各描述符。
图17-4 为协作进程所作的描述符安排
s_pipe函数定义为与标准pipe函数类似。它的调用参数与pipe相同,但是返回的描述符以读-写模式打开。
实例:基于STREASMS的s_pipe函数
程序17-2 基于STREAMS的s_pipe函数版本(它只是简单地调用创建全双工管道的标准pipe函数)
#include "apue.h" /* * Return a STREAMS-based pipe, with the two file descirptors * returned in fd[0] and fd[1]. */ int s_pipe(int fd[2]) { return(pipe(fd)); }
注意,POSIX.1允许实现支持全双工管道。对于这些实现,filedes[0]和filedes[1]以读/写方式打开(http://www.cnblogs.com/nufangrensheng/p/3560130.html)。但是并不是所有实现都支持pipe创建全双工管道。对于不支持pipe创建全双工管道的系统上面实例运行会出错:“Bad file descriptor”。此时,我们把s_pipe函数中return语句 改为:return(socketpair(AF_UNIX, SOCK_STREAM, 0, fd));即可,这里使用了UNIX域套接字接口,详情请参考高级进程间通信之UNIX域套接字。
1、命名的STREAMS管道
通常,管道仅在相关进程之间使用:子进程继承父进程的管道。在http://www.cnblogs.com/nufangrensheng/p/3561632.html曾介绍,无关进程可以使用FIFO进行通信,但是这仅仅提供单向通信。STREAMS机制提供了一种途径,使得进程可以给予管道一个文件系统中的名字。这就避免了单向FIFO的问题。
我们可以用fattach函数给STREAMS管道一个文件系统中的名字。
#include <stropts.h> int fattach(int filedes, const char *path); 返回值:若成功则返回0,若出错则返回-1
path参数必须引用一个现存的文件,调用进程应当或者拥有该文件并且对它具有写权限,或者正在以超级用户特权运行。
一旦STREAMS管道连接到文件系统名字空间,那么原来使用该名字的底层文件就不再是可访问的。打开该名字的任一进程将能访问相应管道,而不是访问原先的文件。在调用fattach之前打开底层文件的任一进程可以继续访问该文件。确实,一般而言,这些进程并不知道该名字现在引用了另外一个文件。
图17-5显示了连接到路径名/tmp/pipe的一条通道。只有管道的一端连接到文件系统中一个名字上。另一端用来与打开该连接文件名的进程通信。虽然fattach函数可将任何种类的STREAMS文件描述符与文件系统中的一个名字相连接,但它最主要用于将一个名字给予一个STREAMS管道。
图17-5 一条管道安装到文件系统的一个名字上
一个进程可以调用fdetach函数撤销STREAMS管道文件与文件系统中名字的关联关系。
#include <stropts.h> int fdetach(const char *path); 返回值:若成功则返回0,若出错则返回-1
在调用fdetach函数之后,先前依靠打开path而能访问STREAMS管道的进程仍可继续访问该管道,但是在此之后打开path的进程将访问驻留在文件系统中的底层文件。
2、唯一连接
虽然我们可以将STREAMS管道的一端连接到文件系统的名字空间,但是如果多个进程都想要用命名STREAMS管道与服务器进程通信,那么仍然存在问题。若几个客户进程同时将数据写至一管道,那么这些数据就会混合交错。即使我们保证客户进程的字节数小于PIPE_BUF,使得写操作是原子性的,但是仍无法保证服务器进程将数据送回所期望的某个客户进程,也无法保证该客户进程一定会读此消息。当多个客户进程同时读一管道时,我们无法调度具体哪一个客户进程去读我们所发送的消息。
connld STREAMS模块解决了这一问题。在将一个STREAMS管道连接到文件系统的一个名字之前,服务器进程可将connld模块压入要被连接管道的一端。其结果示于图17-6。
在图17-6中,服务器进程已将管道的一端连接至/tmp/pipe。我们用虚线指示客户进程正在打开所连接的STREAMS管道。一旦打开操作完成,则服务器进程、客户进程和STREAMS管道之间的关系示于图17-7中。
图17-7 用connld构造唯一连接
客户进程绝不会接收到它所打开管道端的打开文件描述符。作为替代,操作系统创建了一个新管道,对客户进程返回其一端,作为它打开/tmp/pipe的结果。系统将此新管道另一端的文件描述符经由已存在的连接管道发送给服务器进程,结果在客户进程和服务器进程之间构成了唯一连接。
现在,我们将开发三个函数,使用这些函数可以创建在无关进程之间的唯一连接。这些函数模仿了在http://www.cnblogs.com/nufangrensheng/p/3565858.html中讨论过的面向连接的套接字函数。在此处,我们使用STREAMS管道作为底层通信机制,在高级进程间通信之UNIX域套接字章节我们则将见到用UNIX域套接字实现的同样这三个函数。
#include "apue.h" int serv_listen(const char *name); 返回值:若成功则返回要侦听的文件描述符,若出错则返回负值 int serv_accept(int listenfd, uid_t *uidptr); 返回值:若成功则返回新文件描述符,若出错则返回负值 int cli_conn(const char *name); 返回值:若成功则返回文件描述符,若出错则返回负值
服务器进程调用serv_listen函数声明它要在一个众所周知的名字(文件系统中的某个路径名)上侦听客户进程的连接请求。当客户进程想要连接到服务器进程时,它们将使用该名字。serv_listen函数的返回值是STREAMS管道的服务器进程端。
程序清单17-3 使用STREAMS管道的serv_listen函数
#include "apue.h" #include <fcntl.h> #include <stropts.h> /* pipe permissions: user rw, group rw, others rw */ #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) /* * Establish an endpoint to listen for connect requests. * Returns fd if all ok, <0 on error */ int serv_listen(const char *name) { int tempfd; int fd[2]; /* * Create a file: mount point for fattach(). */ unlink(name); if((tempfd = creat(name, FIFO_MODE)) < 0) return(-1); if(close(tempfd) < 0) return(-2); if(pipe(fd) < 0) return(-3); /* * Push connld & fattach() on fd[1]. */ if(ioctl(fd[1], I_PUSH, "connld") < 0) { close(fd[0]); close(fd[1]); return(-4); } if(fattach(fd[1], name) < 0) { close(fd[0]); close(fd[1]); return(-5); } close(fd[1]); /* fattach holds this end open */ return(fd[0]); /* fd[0] is where client connections arrive */ }
服务器进程使用serv_accept函数等待客户进程连接请求的到达。当一个请求到达时,系统自动创建一个新的STREAMS管道,serv_accept函数向服务器进程返回该STREAMS管道的另一端。另外,客户进程的有效用户ID存放在uidptr指向的存储区中。
程序清单17-4 使用STREAMS管道的serv_accept函数
#include "apue.h" #include <stropts.h> /* * Wait for a client connection to arrive, and accept it. * We also obtain the client's user ID. * Return new fd if all ok, <0 on error. */ int serv_accept(int listenfd, uid_t *uidptr) { struct strrecvfd recvfd; if(ioctl(listenfd, I_RECVFD, &recvfd) < 0) return(-1); /* could be EINTR if signal caught */ if(uidptr != NULL) *uidptr = recvfd.uid; /* effective uid of caller */ return(recvfd.fd); /* return the new descriptor */ }
客户进程调用cli_conn函数连接至服务器进程。客户进程指定的参数name必须与服务器进程调用serv_listen函数时所用的相同。函数返回时,客户进程得到连接至服务器进程的文件描述符。
程序清单17-5 用STREAMS管道的cli_conn函数
#include "apue.h" #include <fcntl.h> #include <stropts.h> /* * Create a client endpoint and connect to a server. * Return fd if all ok, <0 on error. */ int cli_conn(const char *name) { int fd; /* open the mounted stream */ if((fd = open(name, O_RDWR)) < 0) return(-1); if(isastream(fd) == 0) { close(fd); return(-2); } return(fd); }
我们对返回的描述符是否引用STREAMS设备进行了二次检验,以防止服务器进程没被启动而路径名仍存在于文件系统中。
本篇博文内容摘自《UNIX环境高级编程》(第二版),仅作个人学习记录所用。关于本书可参考:http://www.apuebook.com/。