BBS水木清华站∶精华区

发信人: SuperSB (孤鹰), 信区: Linux        
标  题: [转载]unix环境高级编程-15 
发信站: BBS 水木清华站 (Wed Mar 15 14:31:19 2000) 
 
 
 
 
发信人: taosm (128+64-->cool), 信区: unix  
标  题: unix环境高级编程--第15章 高级进程间通信  
发信站: 西十八BBS (Sat Mar 11 14:00:43 2000), 转信  
   
第十五章  高级进程间通信  
15.1 引言  
上一章说明了各种UNIX系统提供的IPC经典方法,包括:管道、FIFO、消息队列、信  
号量和共享存储。本章介绍某些高级的IPC以及它们的应用方法,包括:流管道和命  
名流管道。使用这些机制,我们可以在进程间传送打开文件描述符。在分别为每一  
个客户进程提供一个通道的系统中,这些通信机制使客户进程能与精灵服务进程会  
合。4。2BSD和SVR3。2最早提供这些高级形式的IPC,但是至今尚未广泛使用,也缺  
少参政文献。本章中很多思想来自Pressotto和Ritchie[1990]的论文。  
15.2 流管道  
流管道是一个双向(全双工)管道。单个流管道就能向父、子进程提供双向的数据流  
。图15。1显示了观察流管道的两种方式。它与图14.2的唯一区别是双向箭头连线  
,这是因为流管道是全双工的。  
    实例  
我们用一个流管道再次实现了程序14.9的协作进程实例。程序15.1是新的main函数  
。add2协作进程与程序14.8中的相同。程序15.1调用了创建一个流管道的新函数s  
_pipe。(在下面将说明该函数的SVR4和4.3+BSD版本。)  
图15.1  观察流管道的两种方式  
#include <signal.h>  
#include "ourhdr.h"  
static void sig_pipe(int);  /* our signal handler */  
int  
main(void)  
{  
 int  n, fd[2];  
 pid_t pid;  
 char line[MAXLINE];  
 if (signal(SIGPIPE, sig_pipe) == SIG_ERR)  
  err_sys("signal error");  
 if (s_pipe(fd) < 0)   /* only need a single stream pipe */  
  err_sys("pipe error");  
 if ( (pid = fork()) < 0)  
  err_sys("fork error");  
 else if (pid > 0) {       /* parent */  
  close(fd[1]);  
  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 {         /* child */  
  close(fd[0]);  
  if (fd[1] != STDIN_FILENO) {  
   if (dup2(fd[1], STDIN_FILENO) != STDIN_FILENO)  
    err_sys("dup2 error to stdin");  
  }  
  if (fd[1] != STDOUT_FILENO) {  
   if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)  
    err_sys("dup2 error to stdout");  
  }  
  if (execl("./add2", "add2", NULL) < 0)  
   err_sys("execl error");  
 }  
}  
static void  
sig_pipe(int signo)  
{  
 printf("SIGPIPE caught\n");  
 exit(1);  
}  
程序15.1   用流管道驱动add2过滤进程的程序  
    父程序只使用fd[0],子程序只使用fd[1] 。因为流管道的每一端都是全双工的  
,所以父进程读、写fd[0],而子程序将fd[1]复制到标准输入和标准输出。图15.2显  
示了由此构成的各描述符。  
图15.2  为协作进程安排的各描述符  
s_pipe函数定义为与标准pipe函数类似。它的调用参数与pipe相同,但返回的描述  
符以读 写方式打开。  
实例一SVR4下的s_pipe函数  
程序15.2是s_pipe函数的SVR4版本。它只是调用创建全双工管道的标准pipe函数  
#include "ourhdr.h"  
int  
s_pipe(int fd[2]) /* two file descriptors returned in fd[0] & fd[1] */  
{  
 return( pipe(fd) );  
}  
程序15.2  s_pipe函数的SVR4版本  
在系统V的早期版本中也可以创建流管道,但要进行的处理较多。有关SVR3.2下创建  
流管道的详细情况,请参阅Stevens[1990]。  
图15.3显示SVR4之下管道的基本结构。它主要是两个相互连接的流首。  
图15.3  在SVR4之下的管道  
因为管道是一种流设备,我们可将处理模块压入管道的任一端。在15.5.1节,我们将  
用此技术提供一个可以装配的命名管道。  
实例一4.3+BSD之下的s_pipe函数  
程序15.3是s_pipe函数的BSD版本。此函数在4.2BSD及以后的各版本中起作用。它  
创建一对互连的UNIX域流套接口。  
    自4.2BSD开始,常规的管道已用此方式实现。但是,当调用pipe时,第一个描述  
符的写端和第二个描述符的读端都被关闭。为获得全双工管道,必须直接调用sock  
etpair。  
#include <sys/types.h>  
#include <sys/socket.h>  
#include "ourhdr.h"  
int  
s_pipe(int fd[2]) /* two file descriptors returned in fd[0] & fd[1] */  
{  
 return( socketpair(AF_UNIX, SOCK_STREAM, 0, fd) );  
}  
程序15.3  s_pipe函数的BSD版本。  
15.3 传送文件描述符  
    在进程间传送打开文件描述符的能力是非常有用的。用此可以对客户/服务器  
应用进行不同的设计。它允许一个进程(典型地是一个服务器)处理与打开一个文件  
有关的所有操作(涉及的细节可能是:将网络名翻译为网络地址、拨号调制解调器、  
协商文件锁等。)以及向调用进程返回一描述符,该描述符可被用于以后的所有I/O  
函数。打开文件或 设备的所有细节对客户而言都是透明的。  
        4.2BSD支持传送打开描述符,但其实施中有些错误。4.3BSD排除了这些错  
误。3.2  
        及以上版本都支持传送打开描述符。  
下面进一步说明"从一个进程向另一个进程传送一打开文件描述符"的含义。回忆图  
3.3,其中显示了两个进程,它们打开了同一文件。虽然它们共享同一v_node,但每个  
进程都有它自己的文件表项。当从一个进程向另一个进程传送一打开文件描述符时  
,我们想要发送进程和接收进程共享同一文件表项。图15.4示出了所希望的安排。  
在技术上,发送进程实际上向接受进程传送一个指向一打开文件表项的指针。该指  
针被分配存放在接收进程的第一个可用描述符项中。(注意,不要得到错觉以为发送  
进程和接收进程中的描述符编号是相同的,通常它们是不同的。)这种情况与在for  
k之后,父、子进程完全共享一个打开文件表项相同(回忆图8.1)。  
当发送进程将描述符传送给接收进程后,通常它关闭该描述符。发送进程关闭该描  
述符并不造成关闭该文件或设备,其原因是该描述符对应的文件仍需为接收进程打  
开(即使接收进程尚未接收到该描述符)。  
图15.4  从上一进程传送一个打开文件至下一进程  
下面我们定义本章使用的三个函数(在第十八章也使用)以发送和接收文件描述符。  
本节将会给出对于SVR4和4.3+BSD的这三个函数的不同实现。  
_______________________________________________________________________  
______  
#include "ourhdr."  
int send_fd(int spipefd, int filedes);  
int send_err(int spipefd,int status, const char *errmsg);  
 两个函数返回:若成功为0,出错为-1  
int recv_fd(int spipefd, ssize_t (*userfunc)(int , const void *, size-t  
));  
 返回:若成功为文件描述符,出错<0  
_______________________________________________________________________  
______  
当一个进程(通常是一个服务器)希望将一个描述符传送给另一个进程时,它调用se  
nd_fd或send_err。等待接收描述符的进程(客户)调用recv_fd。  
Send_fd经由流管道spipefd发送描述符filedes。send_err 经由流管道spipefd发  
送errmsg和status字节。status的值应在-1至-225之间  
客户调用secv_fd接收一描述符。如果一切正常(发送者调用了send_fd),则作为函  
数值返回非负描述符。否则,返回值是由send_err发送的status(在-1和-255之间的  
一个负值)。另外,如果服务器发送了一条出错消息,则客户调用它自己的userfunc  
处理该消息。userfunc的第一个参数是常数STDERR_FILENO, 然后是指向出错消息  
的指针及其长度。客户常将userfunc指定为UNIX的Write函数。  
我们实现了用于这三个函数的我们自己制定的协议。为发送一描述符,send_fd先发  
送两个0字节,然后是实际描述符。为了发送一条出错消息,send_err 发送errmsg,  
然后是1个0字节,最后是status字节的绝对值(1-255)。recv_fd读流管道中所有字  
节直至null字符。在null字符之前的所有字符都送给调用者的userfunc。recv_fd  
读到的下一个字节是status字节.若status字节为0,那么一个描述符已传送,否  
则表示没有接收到描述符.  
Send_err函数在将出错消息写到流管道后,即调用send_fg函数.这示于程序15  
.4中。  
#include "ourhdr.h"  
/* Used when we had planned to send an fd using send_fd(),  
 * but encountered an error instead.  We send the error back  
 * using the send_fd()/recv_fd() protocol. */  
int  
send_err(int clifd, int errcode, const char *msg)  
{  
 int  n;  
 if ( (n = strlen(msg)) > 0)  
  if (writen(clifd, msg, n) != n) /* send the error message */  
   return(-1);  
 if (errcode >= 0)  
  errcode = -1; /* must be negative */  
 if (send_fd(clifd, errcode) < 0)  
  return(-1);  
 return(0);  
}  
程序15.4 send_err函数  
15.3.1 SVR4  
在SVR4之下,文件描述符用两条ioctl命令在一流管道中交换,这两条命令是:I_  
SENDFD 和I_RECVFD。为了发送一描述符,我们将ioctl的第三个参数设置为实际描  
述符。这示于程序15.5中。  
#include <sys/types.h>  
#include <stropts.h>  
#include "ourhdr.h"  
/* Pass a file descriptor to another process.  
 * If fd<0, then -fd is sent back instead as the error status. */  
int  
send_fd(int clifd, int fd)  
{  
 char buf[2];  /* send_fd()/recv_fd() 2-byte protocol */  
 buf[0] = 0;   /* null byte flag to recv_fd() */  
 if (fd < 0) {  
  buf[1] = -fd; /* nonzero status means error */  
  if (buf[1] == 0)  
   buf[1] = 1; /* -256, etc. would screw up protocol */  
 } else {  
  buf[1] = 0;  /* zero status means OK */  
 }  
 if (write(clifd, buf, 2) != 2)  
  return(-1);  
 if (fd >= 0)  
  if (ioctl(clifd, I_SENDFD, fd) < 0)  
   return(-1);  
 return(0);  
}  
程序15.5  SVR4下的send_fd函数  
当接收一描述符时,ioctl的第三个参数是一指向strrecvfd结构的指针。  
_______________________________________________________________________  
______  
struct strrecvfd {  
 int   fd;      新描述符  
 uid_t  uid;      发送者的有效用户ID  
 gid_t gid;      发送者的有效组ID  
 char  fill[8];  
}  
_______________________________________________________________________  
______  
recv_fd读流管道直到接收到双字节协议的第一个字节(null字节).当发出带I_  
RECVFD命令的ioctl时,在流读首处的第一条消息应当是一个描述符,它是由I_SE  
NDFD发来的,或者得到一条出错消息。这示于程序15.6中。  
#include <sys/types.h>  
#include <stropts.h>  
#include "ourhdr.h"  
/* Receive a file descriptor from another process (a server).  
 * In addition, any data received from the server is passed  
 * to (*userfunc)(STDERR_FILENO, buf, nbytes).  We have a  
 * 2-byte protocol for receiving the fd from send_fd(). */  
int  
recv_fd(int servfd, ssize_t (*userfunc)(int, const void *, size_t))  
{  
 int     newfd, nread, flag, status;  
 char    *ptr, buf[MAXLINE];  
 struct strbuf  dat;  
 struct strrecvfd recvfd;  
 status = -1;  
 for ( ; ; ) {  
  dat.buf = buf;  
  dat.maxlen = MAXLINE;  
  flag = 0;  
  if (getmsg(servfd, NULL, &dat, &flag) < 0)  
   err_sys("getmsg error");  
  nread = dat.len;  
  if (nread == 0) {  
   err_ret("connection closed by server");  
   return(-1);  
  }  
   /* See if this is the final data with null & status.  
      Null must be next to last byte of buffer, status  
      byte is last byte.  Zero status means there must  
      be a file descriptor to receive. */  
  for (ptr = buf; ptr < &buf[nread]; ) {  
   if (*ptr++ == 0) {  
    if (ptr != &buf[nread-1])  
     err_dump("message format error");  
     status = *ptr & 255;  
     if (status == 0) {  
     if (ioctl(servfd, I_RECVFD, &recvfd) < 0)  
      return(-1);  
     newfd = recvfd.fd; /* new descriptor */  
    } else  
     newfd = -status;  
    nread -= 2;  
   }  
  }  
  if (nread > 0)  
   if ((*userfunc)(STDERR_FILENO, buf, nread) != nread)  
    return(-1);  
  if (status >= 0) /* final data has arrived */  
   return(newfd); /* descriptor, or -status */  
 }  
}  
程序15.6  SVR4下的recv_fd函数  
15.3.2 4.3BSD  
    不幸,对于4.3BSD以及在其基础上构造的SunOS和Ultrix,以及从4.3BSD  
Reno开始的后续版本我们必须提供不同的实现。  
为了交换文件描述符,调用sendmsg(2)和recvmsg(2)函数。这两个函数的参数中都  
有一个指向msghdr的指针,该结构包含了所有关于要发送和接收消息的信息。该结  
构定义在〈sys/socket.h〉 头文件中,在BSD4.3之下,其样式是:  
_______________________________________________________________________  
______  
strcut msghdr {  
 caddr_t  msg_name;    可选的地址  
 int   msg_namelen;    地址长度  
 struct iovec  msg_iov;     散布/聚集数组  
 int    msg_iovlen;    在msg_iov数组中的元素数  
 caddr_t  msg_accrights;    存取权发送/接收到  
 int    msg-accrightslen;   存取权缓存的长度  
}  
_______________________________________________________________________  
______  
头两个元素通常用于在网络连接上发送数据报文,在这里,目的地址可以由每个数  
据报文指定。下面两个元素使我们可以指定缓存的数组(散布读和聚集写),这如  
同我们对readv和writev函数(12.7节)的说明一样。最后两个元素处理存取权的  
传送和接收。当前唯一定义的存取权是文件描述符。存取权仅可跨越一个UNIX域套  
接口传送(亦即,在4.3BSD之下作为流管道我们所使用的)。为了发送或接收一文  
件描述符,将msg_accrights设置为指向该整型描述符,将msg_accrightslen设置  
为描述符的长度(亦即,整型的长度)。仅当此长度非0时,才传送或接收描述符  
。  
程序15.7是4.3BSD下的send_fd函数  
#include <sys/types.h>  
#include <sys/socket.h>  /* struct msghdr */  
#include <sys/uio.h>   /* struct iovec */  
#include <errno.h>  
#include <stddef.h>  
#include "ourhdr.h"  
/* Pass a file descriptor to another process.  
 * If fd<0, then -fd is sent back instead as the error status. */  
int  
send_fd(int clifd, int fd)  
{  
 struct iovec iov[1];  
 struct msghdr msg;  
 char   buf[2]; /* send_fd()/recv_fd() 2-byte protocol */  
 iov[0].iov_base = buf;  
 iov[0].iov_len  = 2;  
 msg.msg_iov     = iov;  
 msg.msg_iovlen  = 1;  
 msg.msg_name    = NULL;  
 msg.msg_namelen = 0;  
 if (fd < 0) {  
  msg.msg_accrights    = NULL;  
  msg.msg_accrightslen = 0;  
  buf[1] = -fd; /* nonzero status means error */  
  if (buf[1] == 0)  
   buf[1] = 1; /* -256, etc. would screw up protocol */  
 } else {  
  msg.msg_accrights    = (caddr_t) &fd; /* addr of descriptor */  
  msg.msg_accrightslen = sizeof(int);  /* pass 1 descriptor */  
  buf[1] = 0;  /* zero status means OK */  
 }  
 buf[0] = 0;   /* null byte flag to recv_fd() */  
 if (sendmsg(clifd, &msg, 0) != 2)  
  return(-1);  
 return(0);  
}  
程序15.7  4.3BDS下的send_fd函数  
在sendnisg调用中,发送双字节协议数据(null和status字节)和描述符。  
为了接收一文件描述符,我们从流管道读,直至读到null字节,它位于最后的sta  
tus字节之前。在此null字节之前的是一条出错消息,它来自发送者。这示于程序  
15.8。  
#include <sys/types.h>  
#include <sys/socket.h>  /* struct msghdr */  
#include <sys/uio.h>   /* struct iovec */  
#include <stddef.h>  
#include "ourhdr.h"  
/* Receive a file descriptor from another process (a server).  
 * In addition, any data received from the server is passed  
 * to (*userfunc)(STDERR_FILENO, buf, nbytes).  We have a  
 * 2-byte protocol for receiving the fd from send_fd(). */  
int  
recv_fd(int servfd, ssize_t (*userfunc)(int, const void *, size_t))  
{  
 int    newfd, nread, status;  
 char   *ptr, buf[MAXLINE];  
 struct iovec iov[1];  
 struct msghdr msg;  
 status = -1;  
 for ( ; ; ) {  
  iov[0].iov_base = buf;  
  iov[0].iov_len  = sizeof(buf);  
  msg.msg_iov     = iov;  
  msg.msg_iovlen  = 1;  
  msg.msg_name    = NULL;  
  msg.msg_namelen = 0;  
  msg.msg_accrights = (caddr_t) &newfd;/* addr of descriptor */  
  msg.msg_accrightslen = sizeof(int);  /* receive 1 descriptor */  
  if ( (nread = recvmsg(servfd, &msg, 0)) < 0)  
   err_sys("recvmsg error");  
  else if (nread == 0) {  
   err_ret("connection closed by server");  
   return(-1);  
  }  
   /* See if this is the final data with null & status.  
      Null must be next to last byte of buffer, status  
      byte is last byte.  Zero status means there must  
      be a file descriptor to receive. */  
  for (ptr = buf; ptr < &buf[nread]; ) {  
   if (*ptr++ == 0) {  
    if (ptr != &buf[nread-1])  
     err_dump("message format error");  
     status = *ptr & 255;  
     if (status == 0) {  
     if (msg.msg_accrightslen != sizeof(int))  
      err_dump("status = 0 but no fd");  
     /* newfd = the new descriptor */  
    } else  
     newfd = -status;  
    nread -= 2;  
   }  
  }  
  if (nread > 0)  
   if ((*userfunc)(STDERR_FILENO, buf, nread) != nread)  
    return(-1);  
  if (status >= 0) /* final data has arrived */  
   return(newfd); /* descriptor, or -status */  
 }  
}  
程序15.8  4.3BSD下的recv_fd函数  
注意,该程序总是准备接收一描述符(在每次调用recvmsg之前,设置msg_accrig  
hts和msg_accrightslen ),但是仅当在返回时msg_accrightslen非0,我们才确实  
接收到一描述符。  
15.3.3 4.3+BSD  
从4.3BSD Reno开始,更改了msghdr结构的定义。在以前版本中被称之为"存取权"  
的最后两个元素改称为"辅助数据"。另外,在该结构结束处增加了一个新成员msg  
_flags。  
strcut msghdr {  
 caddr_t  msg_name;    可选的地址  
 int   msg_namelen;    地址长度  
 struct iovec  msg_iov;     散布/聚集数组  
 int    msg_iovlen;    在msg_iov数组中的元素数  
 caddr_t  msg_control;    辅助数据  
 int    msg-controllen;   辅助数据的长度  
 int    msg_flags;    接收到消息的标志  
}  
现在,msg_control字段指向一个cmsghdr(控制消息头)结构。  
struct cmsghdr {  
 u_int cmsg_len;     数据的字节数,包括头  
 int   cmsg_level;     初始的协议  
 int   cmsg_type;     协议细节的类型  
          下接真正的控制消息数据  
}  
为了发送一文件描述符,将cmsg_len设置为cmsghdr结构长度加一个整型(描述符  
   
)的长度。将cmsg_level设置为SOL_SOCKET,cmsg_type设置为SCM_RIGHTS,这表明  
正在传送的是存取权。("SCM"表示套接口级控制消息,"socket-level control  
message"。)实际描述符的存放位置紧随cmsy_type字段之后,使用CMSG_DATA宏以  
获得指向该整型数的指针。程序15.9示出了4.3BSD Reno之下的send_fd函数。  
#include <sys/types.h>  
#include <sys/socket.h>  /* struct msghdr */  
#include <sys/uio.h>   /* struct iovec */  
#include <errno.h>  
#include <stddef.h>  
#include "ourhdr.h"  
static struct cmsghdr *cmptr = NULL; /* buffer is malloc'ed first time  
*/  
#define CONTROLLEN (sizeof(struct cmsghdr) + sizeof(int))  
  /* size of control buffer to send/recv one file descriptor */  
/* Pass a file descriptor to another process.  
 * If fd<0, then -fd is sent back instead as the error status. */  
int  
send_fd(int clifd, int fd)  
{  
 struct iovec iov[1];  
 struct msghdr msg;  
 char   buf[2]; /* send_fd()/recv_fd() 2-byte protocol */  
 iov[0].iov_base = buf;  
 iov[0].iov_len  = 2;  
 msg.msg_iov     = iov;  
 msg.msg_iovlen  = 1;  
 msg.msg_name    = NULL;  
 msg.msg_namelen = 0;  
 if (fd < 0) {  
  msg.msg_control    = NULL;  
  msg.msg_controllen = 0;  
  buf[1] = -fd; /* nonzero status means error */  
  if (buf[1] == 0)  
   buf[1] = 1; /* -256, etc. would screw up protocol */  
 } else {  
  if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)  
   return(-1);  
  cmptr->cmsg_level  = SOL_SOCKET;  
  cmptr->cmsg_type   = SCM_RIGHTS;  
  cmptr->cmsg_len    = CONTROLLEN;  
  msg.msg_control    = (caddr_t) cmptr;  
  msg.msg_controllen = CONTROLLEN;  
  *(int *)CMSG_DATA(cmptr) = fd;  /* the fd to pass */  
  buf[1] = 0;  /* zero status means OK */  
 }  
 buf[0] = 0;   /* null byte flag to recv_fd() */  
 if (sendmsg(clifd, &msg, 0) != 2)  
  return(-1);  
 return(0);  
}  
程序15.9  4.3BSD之下的send_fd函数  
为了接收一描述符(程序15.10),我们为cmsghdr结构和一描述符分配足够的存储  
区,设置msg_control使其指向所分配到的存储区,然后调用recvmsg。  
#include <sys/types.h>  
#include <sys/socket.h>  /* struct msghdr */  
#include <sys/uio.h>   /* struct iovec */  
#include <stddef.h>  
#include "ourhdr.h"  
static struct cmsghdr *cmptr = NULL;  /* malloc'ed first time */  
#define CONTROLLEN (sizeof(struct cmsghdr) + sizeof(int))  
   /* size of control buffer to send/recv one file descriptor */  
/* Receive a file descriptor from another process (a server).  
 * In addition, any data received from the server is passed  
 * to (*userfunc)(STDERR_FILENO, buf, nbytes).  We have a  
 * 2-byte protocol for receiving the fd from send_fd(). */  
int  
recv_fd(int servfd, ssize_t (*userfunc)(int, const void *, size_t))  
{  
 int    newfd, nread, status;  
 char   *ptr, buf[MAXLINE];  
 struct iovec iov[1];  
 struct msghdr msg;  
 status = -1;  
 for ( ; ; ) {  
  iov[0].iov_base = buf;  
  iov[0].iov_len  = sizeof(buf);  
  msg.msg_iov     = iov;  
  msg.msg_iovlen  = 1;  
  msg.msg_name    = NULL;  
  msg.msg_namelen = 0;  
  if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)  
   return(-1);  
  msg.msg_control    = (caddr_t) cmptr;  
  msg.msg_controllen = CONTROLLEN;  
  if ( (nread = recvmsg(servfd, &msg, 0)) < 0)  
   err_sys("recvmsg error");  
  else if (nread == 0) {  
   err_ret("connection closed by server");  
   return(-1);  
  }  
   /* See if this is the final data with null & status.  
      Null must be next to last byte of buffer, status  
      byte is last byte.  Zero status means there must  
      be a file descriptor to receive. */  
  for (ptr = buf; ptr < &buf[nread]; ) {  
   if (*ptr++ == 0) {  
    if (ptr != &buf[nread-1])  
     err_dump("message format error");  
     status = *ptr & 255;  
     if (status == 0) {  
     if (msg.msg_controllen != CONTROLLEN)  
      err_dump("status = 0 but no fd");  
     newfd = *(int *)CMSG_DATA(cmptr); /* new descriptor */  
    } else  
     newfd = -status;  
    nread -= 2;  
   }  
  }  
  if (nread > 0)  
   if ((*userfunc)(STDERR_FILENO, buf, nread) != nread)  
    return(-1);  
  if (status >= 0) /* final data has arrived */  
   return(newfd); /* descriptor, or -status */  
 }  
}  
程序15.10  4.3BSD之下的recv_fd函数  
15.4 Open服务器,版本1  
    使用文件描述符传送技术,现在我们开发一个open服务器:它是一个可执行程  
序,  
由一个进程执行以打开一个或多个文件。该服务器不是将这种文件送回调用进程,  
而是送回一个打开文件描述符。这使该服务器对任何类型的文件(例如调制解调器  
线,或一网络连接)而不单是普通文件都能起作用。这也意味着,用IPC交换最小  
量的信息--从客户到服务器传送文件名和打开方式,而从服务器到客户返回描述符  
。文件内容则不需用IPC传送。  
  将服务器设计成一个单独的可执行程序有很多优点:  
1. 任一客户都易于和服务器联系,这类似于客户调用一库函数。我们不需要将一  
特定服务编码在应用程序中,而是设计一种可供重用的设施。  
2. 如若需要更改服务器,那么也只影响一道程序。相反,更新一库函数可能要更  
改调用此库函数的所有程序(用连编程序重新连接)。共享库函数可以简化这种更  
新。  
3. 服务器可以是设置__用户__ID程序,于是使其具有客户没有的附加许可权。注  
意,一个库函数(或共享库函数)不能提供这种能力。  
   客户进程创建一流管道,然后调用fork和exec以调用服务器。客户经流管道发  
送请求,服务器经管道回送响应。我们定义客户和服务器间的协议如下。  
1. 客户经流管道向服务器发送下列形式的请求:  
      open  <pathname>  <openmode>\o  
<openmode>是open函数的第二个参数,以十进制表示。该请求字符串以null字节结  
尾。  
2. 服务器调用send_fd 或send_err回送一打开描述符或一条出错消息。这是一个  
进程向其父进程发送一打开描述符的实例。在15.6节,我们将修改此实例,其中使  
用了一个精灵服务器,它将一个描述符发送给完全无关的进程。  
     程序15.11是头文件open.h,它包括标准系统头文件,并且定义了各个函数原  
型。  
#include <sys/types.h>  
#include <errno.h>  
。文件内容则不需用IPC传送。  
  将服务器设计成一个单独的可执行程序有很多优点:  
1. 任一客户都易于和服务器联系,这类似于客户调用一库函数。我们不需要将一  
特定服务编码在应用程序中,而是设计一种可供重用的设施。  
2. 如若需要更改服务器,那么也只影响一道程序。相反,更新一库函数可能要更  
改调用此库函数的所有程序(用连编程序重新连接)。共享库函数可以简化这种更  
新。  
3. 服务器可以是设置__用户__ID程序,于是使其具有客户没有的附加许可权。注  
意,一个库函数(或共享库函数)不能提供这种能力。  
   客户进程创建一流管道,然后调用fork和exec以调用服务器。客户经流管道发  
送请求,服务器经管道回送响应。我们定义客户和服务器间的协议如下。  
1. 客户经流管道向服务器发送下列形式的请求:  
      open  <pathname>  <openmode>\o  
<openmode>是open函数的第二个参数,以十进制表示。该请求字符串以null字节结  
尾。  
2. 服务器调用send_fd 或send_err回送一打开描述符或一条出错消息。这是一个  
进程向其父进程发送一打开描述符的实例。在15.6节,我们将修改此实例,其中使  
用了一个精灵服务器,它将一个描述符发送给完全无关的进程。  
     程序15.11是头文件open.h,它包括标准系统头文件,并且定义了各个函数原  
型。  
#include <sys/types.h>  
#include <errno.h>  
#include "ourhdr.h"  
#define CL_OPEN "open"   /* client's request for server */  
    /* our function prototypes */  
int  csopen(char *, int);  
程序15.11  open.h头文件  
程序15.12是main函数,其中包含一个循环,它先从标准输入读一个路径名,然后  
将该文件复制至标准输出。它调用函数csopen以与open 服务器联系,从其返回一  
打开描述符。  
#include "open.h"  
#include <fcntl.h>  
#define BUFFSIZE 8192  
int  
main(int argc, char *argv[])  
{  
 int  n, fd;  
 char buf[BUFFSIZE], line[MAXLINE];  
     /* read filename to cat from stdin */  
 while (fgets(line, MAXLINE, stdin) != NULL) {  
  line[strlen(line) - 1] = 0; /* replace newline with null */  
     /* open the file */  
  if ( (fd = csopen(line, O_RDONLY)) < 0)  
   continue; /* csopen() prints error from server */  
     /* and cat to stdout */  
  while ( (n = read(fd, buf, BUFFSIZE)) > 0)  
   if (write(STDOUT_FILENO, buf, n) != n)  
    err_sys("write error");  
  if (n < 0)  
   err_sys("read error");  
  close(fd);  
 }  
 exit(0);  
}  
程序15.12  main 函数  
程序15.13是函数csopen,它先创建一流管道,然后进行服务器的fork和exec操作  
。  
#include "open.h"  
#include <sys/uio.h>  /* struct iovec */  
/* Open the file by sending the "name" and "oflag" to the  
 * connection server and reading a file descriptor back. */  
int  
csopen(char *name, int oflag)  
{  
 pid_t   pid;  
 int    len;  
 char   buf[10];  
 struct iovec iov[3];  
 static int  fd[2] = { -1, -1 };  
 if (fd[0] < 0) { /* fork/exec our open server first time */  
  if (s_pipe(fd) < 0)  
   err_sys("s_pipe error");  
  if ( (pid = fork()) < 0)  
   err_sys("fork error");  
  else if (pid == 0) {  /* child */  
   close(fd[0]);  
   if (fd[1] != STDIN_FILENO) {  
    if (dup2(fd[1], STDIN_FILENO) != STDIN_FILENO)  
     err_sys("dup2 error to stdin");  
   }  
   if (fd[1] != STDOUT_FILENO) {  
    if (dup2(fd[1], STDOUT_FILENO) != STDOUT_FILENO)  
     err_sys("dup2 error to stdout");  
   }  
   if (execl("./opend", "opend", NULL) < 0)  
    err_sys("execl error");  
  }  
  close(fd[1]);    /* parent */  
 }  
 sprintf(buf, " %d", oflag);  /* oflag to ascii */  
 iov[0].iov_base = CL_OPEN " ";  
 iov[0].iov_len  = strlen(CL_OPEN) + 1;  
 iov[1].iov_base = name;  
 iov[1].iov_len  = strlen(name);  
 iov[2].iov_base = buf;  
 iov[2].iov_len  = strlen(buf) + 1; /* +1 for null at end of buf */  
 len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;  
 if (writev(fd[0], &iov[0], 3) != len)  
  err_sys("writev error");  
   /* read descriptor, returned errors handled by write() */  
 return( recv_fd(fd[0], write) );  
}  
                     程序15.13   csopen函数  
子进程关闭管道的一端,父进程关闭另一端。子进程也为它所执行的服务器将管道  
它使用的一端复制到其标准输入和标准输出0(另一种可选择的方案是将描述符fd  
[1]的ASCII 表示形式作为一个参数传送给服务器。)  
父进程将请求发送给服务器,请求中包含路径名和打开方式。最后,父进程调用r  
ecv-fd以返回描述符或错误消息。如若服务器返回一错误消息则调用write,向标  
准出错输出该消息。  
现在,观察open服务器。其程序是opend,它由子进程执行(见程序15.13)。先观  
察opend.h头文件(程序15.14),它包括了系统头文件,并且说明了全局变量和函  
数原型。  
#include <sys/types.h>  
#include <errno.h>  
#include "ourhdr.h"  
#define CL_OPEN "open"    /* client's request for server */  
   /* declare global variables */  
extern char  errmsg[]; /* error message string to return to client */  
extern int  oflag;  /* open() flag: O_xxx ... */  
extern char *pathname; /* of file to open() for client */  
   /* function prototypes */  
int   cli_args(int, char **);  
void  request(char *, int, int);  
程序.15.14 opend.h头文件  
#include "opend.h"  
   /* define global variables */  
char  errmsg[MAXLINE];  
int   oflag;  
char *pathname;  
int  
main(void)  
{  
 int  nread;  
 char buf[MAXLINE];  
 for ( ; ; ) { /* read arg buffer from client, process request */  
  if ( (nread = read(STDIN_FILENO, buf, MAXLINE)) < 0)  
   err_sys("read error on stream pipe");  
  else if (nread == 0)  
   break;  /* client has closed the stream pipe */  
  request(buf, nread, STDIN_FILENO);  
 }  
 exit(0);  
}  
程序15.15 main函数  
#include "opend.h"  
#include <fcntl.h>  
void  
request(char *buf, int nread, int fd)  
{  
 int  newfd;  
 if (buf[nread-1] != 0) {  
  sprintf(errmsg, "request not null terminated: %*.*s\n",  
         nread, nread, buf);  
  send_err(STDOUT_FILENO, -1, errmsg);  
  return;  
 }  
   /* parse the arguments, set options */  
 if (buf_args(buf, cli_args) < 0) {  
  send_err(STDOUT_FILENO, -1, errmsg);  
  return;  
 }  
 if ( (newfd = open(pathname, oflag)) < 0) {  
  sprintf(errmsg, "can't open %s: %s\n",  
        pathname, strerror(errno));  
  send_err(STDOUT_FILENO, -1, errmsg);  
  return;  
 }  
   /* send the descriptor */  
 if (send_fd(STDOUT_FILENO, newfd) < 0)  
  err_sys("send_fd error");  
 close(newfd);  /* we're done with descriptor */  
}  
程序15.16  request函数  
    主函数(程序15.15)经流管道(它的标准输入)读来自客户的请求,然后调  
用函数request。  
程序15.16中的request 函数承担全部工作。它调用函数buf_args将客户请求分解  
成标准argv型的参数表,然后调用函数cli_args以处理客户的参数。如若一切正常  
,则调用open 以打开相应文件,接着调用send_fd,经由流管道(它的标准输出)  
将描述符回送给客户。如若出错则调用send_err回送一则出错消息,其中使用我们  
在前面说明了的客户/服务器协议。  
#include "ourhdr.h"  
#define MAXARGC  50 /* max number of arguments in buf */  
#define WHITE " \t\n" /* white space for tokenizing arguments */  
/* buf[] contains white-space separated arguments.  We convert it  
 * to an argv[] style array of pointers, and call the user's  
 * function (*optfunc)() to process the argv[] array.  
 * We return -1 to the caller if there's a problem parsing buf,  
 * else we return whatever optfunc() returns.  Note that user's  
 * buf[] array is modified (nulls placed after each token). */  
int  
buf_args(char *buf, int (*optfunc)(int, char **))  
{  
 char *ptr, *argv[MAXARGC];  
 int  argc;  
 if (strtok(buf, WHITE) == NULL)  /* an argv[0] is required */  
  return(-1);  
 argv[argc = 0] = buf;  
 while ( (ptr = strtok(NULL, WHITE)) != NULL) {  
  if (++argc >= MAXARGC-1) /* -1 for room for NULL at end */  
   return(-1);  
  argv[argc] = ptr;  
 }  
 argv[++argc] = NULL;  
 return( (*optfunc)(argc, argv) );  
   /* Since argv[] pointers point into the user's buf[],  
      user's function can just copy the pointers, even  
      though argv[] array will disappear on return. */  
}  
程序15.17 buf_args函数  
  buf_args调用的服务器函数是cli_args(程序15.18)。它验证客户发送的参  
数数是否正确,然后将路径名和打开方式存放在全局变量中。  
  这样也就完成了open服务器,它由客户执行fork和exec而调用。在fork之前创  
建了一个流管道,然后客户和服务器用其进行通信。在这种安排下,每个客户都有  
一服务器。  
在下一节观察了客户一服务器连接后,我们将在15.6节重新实现一个open服务器,  
其中用一个精灵进程作为服务器,所有客户都与其进行联系。  
#include "opend.h"  
/* This function is called by buf_args(), which is called by  
 * request().  buf_args() has broken up the client's buffer  
 * into an argv[] style array, which we now process. */  
int  
cli_args(int argc, char **argv)  
{  
 if (argc != 3 || strcmp(argv[0], CL_OPEN) != 0) {  
  strcpy(errmsg, "usage: <pathname> <oflag>\n");  
  return(-1);  
 }  
 pathname = argv[1];  /* save ptr to pathname to open */  
 oflag = atoi(argv[2]);  
 return(0);  
}  
程序15.18  cli_args函数  
15.5     客户-服务器连接函数  
     对于相关进程(例如,父进程和子进程)之间的IPC,流管道是非常有用的。  
前节所述的open服务器使用末命名的流管道能从子进程向父进程传送文件描述符。  
但是当我们处理无关进程时(例如,若服务器是一精灵进程),则需要使用有名的  
流管道。  
     我们可以先构造一末名流管道(用s_pipe 函数),然后对每一端加上一文件  
系统路径名。一精灵服务器进程将只创建流管道的一端,并对该端加上一名字。这  
样,无关的客户可以向服务者的流管道端发送消息,从而与精灵进程会聚。这类似  
于图14.12中所示的情况,在该图中客户使用FIFO发送它们的请求。  
     一种更好的方法是:服务器创建一名字公开的流管道的一端,然后客户连接  
_______________________________________________________________________  
______  
    listenfd是serv_listen返回的描述符。在一客户连接到服务器的众所周知的  
名字上之前,此函数并不返回。当客户连接至服务器时,自动创建一条全新的流管  
道,其新描述符作为该函数的值返回。另外,客户的有效用户ID通过指针uidptr存  
储。  
客户为与一服务器连接只需调用cli_conn函数。  
_______________________________________________________________________  
______  
#include "ourhdr,h"  
int cli_conn(const char *name);  
      返回:若成功返回为文件描述符,出错<0  
_______________________________________________________________________  
______  
客户指定的name应当与服务器调用serv_listen时宣布的相同。返回的描述符引用  
连接至服务器的流管道  
使用上述三个函数,就可编写服务器精灵进程,它可以管理任意数量的客户。唯一  
的限制是单个进程可用的描述符数,服务器对于每一个客户连接都需要一个描述符  
。因为这些函数处理的都是普通文件描述符,所以服务器使用SELECT或POLL就可在  
所有客户间多路转接I/O请求。最后,因为客户一服务器连接都是流管道,所以可  
以经由连接传送打开描述符。  
在下面二节中,将说明在SVR4和4.3+BSD之下这三个函数的实现。在第十八章中当  
我们开发一个通用的连接服务器时,也将使用这三个函数。  
15.5.1 SVR4  
    SVR4提供装配的流以及一个名为connld的流处理模块,用其可以提供与服务器  
有唯一连接的命名流管道。  
        装配流和connld模块是由Presotto和Ritchie[1990]为RESEARCH UNIX系统  
开发  
        的,后来由SVR4采用。  
首先,服务器创建一末名流管道,并将流处理模块connld压入一端。图15.5 显示  
了这一处理结果。  
图15.5  在一端压入connld模块后的流管道  
然后,使压入connld的一端具有一路径名。SVR4提供fattach函数实现这一点。任  
一进程(例如客户)打开此路径名就引用该管道的命名端。程序15.19使用了二十  
余行代码实现serv_listen函数。  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <stropts.h>  
#include "ourhdr.h"  
#define FIFO_MODE  (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)  
     /* user rw, group rw, others rw */  
int   /* returns fd if all OK, <0 on error */  
serv_listen(const char *name)  
{  
 int  tempfd, fd[2], len;  
     /* 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)  
  return(-4);  
 if (fattach(fd[1], name) < 0)  
  return(-5);  
 return(fd[0]); /* fd[0] is where client connections arrive */  
}  
程序15.19   SVR4之下的serv_listen函数  
     当另一进程对管道的命名端(connld模块压入端)调用open时,发生下列处  
理过程:  
1. 创建一个新管道。  
2. 该新管道的一个描述符作为open的返回值回送给客户。  
3. 另一个描述符在命名管道的另一端(亦即不是压入connld的端)传送给服务器  
。服务器以带I-RECVED命令的ioctl接受该新描述符。  
      假定,服务器用fattach函数加到其管道的众所周知是/tmp/serv1.图15.6显  
示了客户调用  
      fd=open("/tmp/serv1",O_RDWR);  
并返回后产生的结果。  
图15.6 客户-服务器在一命名管道上的连接  
在客户和服务器之间的管道是open创建的,被打开的路径名实际上是一命名管道,  
其中压入了connld模块。客户得到由open返回的文件描述符fd。在服务器处的新文  
件描述符是clifdl,它是由服务器在描述符fd[1]上以I_RECVFD命令调用ioctl而接  
收到的。一旦服务器在fd[1]上压入了connld模块,并对fd[1]附接上一个名字,它  
就不再使用fd[1]。  
服务器调用程序15.20中的serv_accept函数等待客户连接到达。  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <stropts.h>  
#include "ourhdr.h"  
/* Wait for a client connection to arrive, and accept it.  
 * We also obtain the client's user ID. */  
int   /* returns new fd if all OK, -1 on error */  
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 */  
}  
程序15.20 用于SVR4的serv_accept函数  
    在图15.6中,serv_accept的第一个参数应当是描述符fd[0],serv_accept的  
返回值是描述符clifdl。  
客户调用程序15.21中的cli_conn函数起动对服务器的连接。  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include "ourhdr.h"  
/* Create a client endpoint and connect to a server. */  
int   /* returns fd if all OK, <0 on error */  
cli_conn(const char *name)  
{  
 int  fd;  
    /* open the mounted stream */  
 if ( (fd = open(name, O_RDWR)) < 0)  
  return(-1);  
 if (isastream(fd) == 0)  
  return(-2);  
 return(fd);  
}  
程序15.21 对于SVR4的cli_conn函数  
我们对返回的描述符是否引用一个流设备进行了两次检查,以便处理服务器没有起  
动,但该路径名却存在于文件系统中的情况。(在SVR4下,几乎没有什么理由去调  
用cli_conn,而不是直接调用open。在下一节我们将看到,在BSD系统之下,cli_  
conn函数要复杂得多,因此编写cli_conn函数就是必要的了。)  
15.5.2 4.3+BSD  
在4.3+BSD之下,为了用UNIX域套接口连接客户和服务器,我们需要有一套不同的  
操作函数。因为应用socket、bind、listen、accept和connect函数的大部分细节  
与其它网络协议有关(参见Stevens[1990]),所以此处不详细展开。  
        因为SVR4也支持UNIX域套接口,所以本节所示代码同样可在SVR4之下工作  
。  
    程序15.22包含了serv_listen函数。它是服务器调用的第一个函数  
#include <sys/types.h>  
#include <sys/socket.h>  
#include <sys/un.h>  
#include "ourhdr.h"  
/* Create a server endpoint of a connection. */  
int   /* returns fd if all OK, <0 on error */  
serv_listen(const char *name)  
{  
 int     fd, len;  
 struct sockaddr_un unix_addr;  
     /* create a Unix domain stream socket */  
 if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)  
  return(-1);  
 unlink(name); /* in case it already exists */  
     /* fill in socket address structure */  
 memset(&unix_addr, 0, sizeof(unix_addr));  
 unix_addr.sun_family = AF_UNIX;  
 strcpy(unix_addr.sun_path, name);  
#ifdef SCM_RIGHTS /* 4.3BSD Reno and later */  
 len = sizeof(unix_addr.sun_len) + sizeof(unix_addr.sun_family) +  
    strlen(unix_addr.sun_path) + 1;  
 unix_addr.sun_len = len;  
#else    /* vanilla 4.3BSD */  
 len = strlen(unix_addr.sun_path) + sizeof(unix_addr.sun_family);  
#endif  
     /* bind the name to the descriptor */  
 if (bind(fd, (struct sockaddr *) &unix_addr, len) < 0)  
  return(-2);  
 if (listen(fd, 5) < 0) /* tell kernel we're a server */  
  return(-3);  
 return(fd);  
}  
程序15.22 用于4.3+BSD的serv_listen函数  
    首先,调用socket函数创建一个UNIX域套接口。然后,填充sockeraddr_un结  
构,将一个众所周知的路径名赋与该套接口。该结构是调用bind函数的一个参数。  
然后调用listen以通知核心:本服务器正等待来自客户的连接。(listen的第二个  
参数是5,它是最大的未决连接请求数,核心将这些请求对该描述符进行排队。大  
多数实现强制该值的上限为5。)  
客户调用cli_conn函数(程序15.23)起动与服务器的连接。  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include "ourhdr.h"  
/* Create a client endpoint and connect to a server. */  
int   /* returns fd if all OK, <0 on error */  
cli_conn(const char *name)  
{  
 int  fd;  
    /* open the mounted stream */  
 if ( (fd = open(name, O_RDWR)) < 0)  
  return(-1);  
 if (isastream(fd) == 0)  
  return(-2);  
 return(fd);  
}  
程序15.23 用于4.3+BSD的cli_conn函数  
我们调用socket函数以创建客户端的UNIX域套接口,然后客户专用的名字填入soc  
ketaddr_un结构。该路径名的最后5个字符是客户的进程ID。(我们可以查证此结  
构的长度是14个字符,以避免UNIX域套接口早期实现的某些错误。)在路径名已经  
存在的情况下unlink,然后再调用bind将一名字赋与客户的套接口,这就创建了在  
文件系统中的路径名,该文件的类型是套接口。接着调用chmod,它关闭除user_r  
ead,user_write和user_execute以外的存取权。在serv_accept中,服务器检查该  
套接口的这些许可权和用户ID,以验证用户的身份。  
    然后,我们应当以服务器众所周知的路径名填充另一个socketaddr_un结构。  
最后,connect函数起动与服务器的连接。  
创建每个客户与服务器的唯一连接是在serv_accept函数中调用accept函数实现的  
(程序15.24)。  
#include <sys/types.h>  
#include <sys/stat.h>  
#include <stropts.h>  
#include "ourhdr.h"  
/* Wait for a client connection to arrive, and accept it.  
 * We also obtain the client's user ID. */  
int   /* returns new fd if all OK, -1 on error */  
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 */  
}  
程序15.24 用于4.3+BSD的serv_accept函数  
     服务器在调用accept中堵塞以等待一客户调用cli_conn。当accept返回时,  
其返回值是连向客户的全新的描述符。(这类似于SVR4中,connld模块所做的。)  
另外,accept也通过其第二个参数(指向socketaddr_un结构的指针)返回客户赋  
与其套接口的路径名(它包含客户的进程ID)。用NULL字节结束此路径名,然后调  
用stat。这使我们可以验证此路径名确实是一个套接口,其许可权user_read,us  
er_write和user_execute。我们也验证与该套接口相关的三个时间不超过30秒。(  
time函数返回自UNIX纪元经过的时间和日期,它们都以秒计。)如若所有这些检查  
都通过我们就认为该客户的身份(其有效用户ID)是该套接口的属主。虽然这种检  
查并不完善,但却是现有系统所能做得最好的。(如果核心能象SVR4 I_RECVFD做  
的那样,将有效用户ID返回给accept,那就更好一些。)  
图15.7显示了cli_conn调用返回后的这种连接,我们假定服务器的众所周知名字是  
/tmp/servi。请将此与图15.6相比较。  
图15.7 在UNIX域套接口上客户-服务器连接  
15.6 OPEN服务器,版本2  
    在15.4节,客户调用fork和exec构造了一个Open服务器,它说明了如何从子程  
序向父程序传送文件描述符。在本节开发一个精灵进程样式的OPEN服务器。一个服  
务器处理所有客户的请求。我们期望,由于避免使用了fork和exec,所以这一设计  
会是更有效的。在客户和服务器之间仍将使用上一节说明的三个函数:serv_list  
en、serv_accept和cli_conn。这一服务器将表明:一个服务器可以处理多个客户  
,为此使用的技术是12.5节中说明的select和poll函数。  
    本节所述的客户类似于15.4节中的客户。确实,文件main.c是完全相同的(程  
序15.12)。在open.h头文件(程序15.11)中则加了下面1行:  
           #define  CS_OPEN  "/home/stevens/open"   /*服务器的众所周知名  
字*/  
因为在这里调用的是cli_conn而非fork和exec,所以文件open.c与程序15.13完全  
不同。这示于程序15.25。  
#include "open.h"  
#include <sys/uio.h>  /* struct iovec */  
/* Open the file by sending the "name" and "oflag" to the  
 * connection server and reading a file descriptor back. */  
int  
csopen(char *name, int oflag)  
{  
 int    len;  
 char   buf[10];  
 struct iovec iov[3];  
 static int  csfd = -1;  
 if (csfd < 0) {  /* open connection to conn server */  
  if ( (csfd = cli_conn(CS_OPEN)) < 0)  
   err_sys("cli_conn error");  
 }  
 sprintf(buf, " %d", oflag);  /* oflag to ascii */  
 iov[0].iov_base = CL_OPEN " ";  
 iov[0].iov_len  = strlen(CL_OPEN) + 1;  
 iov[1].iov_base = name;  
 iov[1].iov_len  = strlen(name);  
 iov[2].iov_base = buf;  
 iov[2].iov_len  = strlen(buf) + 1;  
       /* null at end of buf always sent */  
 len = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;  
 if (writev(csfd, &iov[0], 3) != len)  
  err_sys("writev error");  
     /* read back descriptor */  
     /* returned errors handled by write() */  
 return( recv_fd(csfd, write) );  
}  
程序15.25  csopen函数  
    从客户到服务器之间使用的协议仍然相同。  
    让我们先查看服务器。头文件open.h(程序15.26)包括了标准头文件,并且  
说明了全局变量和函数原型。  
因为此服务器处理所有客户,所以它必须保存每个客户连接的状态。这是用定义在  
opend.h头文件中的client数组实现的。程序15.27定义了三个处理此数组的函数。  
   
#include <sys/types.h>  
#include <errno.h>  
#include "ourhdr.h"  
#define CS_OPEN "/home/stevens/opend" /* well-known name */  
#define CL_OPEN "open"      /* client's request for server */  
   /* declare global variables */  
extern int  debug;  /* nonzero if interactive (not daemon) */  
extern char  errmsg[]; /* error message string to return to client */  
extern int  oflag;  /* open flag: O_xxx ... */  
extern char *pathname; /* of file to open for client */  
typedef struct { /* one Client struct per connected client */  
  int fd;  /* fd, or -1 if available */  
  uid_t uid;  
} Client;  
extern Client *client;  /* ptr to malloc'ed array */  
extern int   client_size; /* # entries in client[] array */  
     /* (both manipulated by client_XXX() functions) */  
   /* function prototypes */  
int   cli_args(int, char **);  
int   client_add(int, uid_t);  
void  client_del(int);  
void  loop(void);  
void  request(char *, int, int, uid_t);  
程序15.26    open.h头文件  
#include "opend.h"  
#define NALLOC 10  /* #Client structs to alloc/realloc for */  
static void  
client_alloc(void)  /* alloc more entries in the client[] array */  
{  
 int  i;  
 if (client == NULL)  
  client = malloc(NALLOC * sizeof(Client));  
 else  
  client = realloc(client, (client_size + NALLOC) * sizeof(Client));  
 if (client == NULL)  
  err_sys("can't alloc for client array");  
   /* have to initialize the new entries */  
 for (i = client_size; i < client_size + NALLOC; i++)  
  client[i].fd = -1; /* fd of -1 means entry available */  
 client_size += NALLOC;  
}  
/* Called by loop() when connection request from a new client arrives *  
/  
int  
client_add(int fd, uid_t uid)  
{  
 int  i;  
 if (client == NULL)  /* first time we're called */  
  client_alloc();  
again:  
 for (i = 0; i < client_size; i++) {  
  if (client[i].fd == -1) { /* find an available entry */  
   client[i].fd = fd;  
   client[i].uid = uid;  
   return(i); /* return index in client[] array */  
  }  
 }  
   /* client array full, time to realloc for more */  
 client_alloc();  
 goto again;  /* and search again (will work this time) */  
}  
/* Called by loop() when we're done with a client */  
void  
client_del(int fd)  
{  
 int  i;  
 for (i = 0; i < client_size; i++) {  
  if (client[i].fd == fd) {  
   client[i].fd = -1;  
   return;  
  }  
 }  
 log_quit("can't find client entry for fd %d", fd);  
}  
程序15.27    处理client数组的三个函数  
    第一次调用client_add时,它调用client_alloc、client_alloc又调用mallo  
c为该数组的10个登记项分配空间。在这十个登记项全部用完后,再调用client_a  
dd,然后是realloc以分配附加空间。依靠这种动态空间分配,我们没有在编译时  
限制client数组的长度。  
 如若出错,那么因为假定服务器是精灵进程,所以这些函数调用log_函数(见附  
录B)。  
    main函数(程序15.28)定义全局变量,处理命令行选择项,然后调用loop函  
数。如若以一d选择项调用服务器,则它以交互方式运行而非精灵进程。当测试些  
服务器时,使用交互运行方式。  
#include "opend.h"  
#include <syslog.h>  
   /* define global variables */  
int   debug;  
char  errmsg[MAXLINE];  
int   oflag;  
char *pathname;  
Client *client = NULL;  
int   client_size;  
int  
main(int argc, char *argv[])  
{  
 int  c;  
 log_open("open.serv", LOG_PID, LOG_USER);  
 opterr = 0;  /* don't want getopt() writing to stderr */  
 while ( (c = getopt(argc, argv, "d")) != EOF) {  
  switch (c) {  
  case 'd':  /* debug */  
   debug = 1;  
   break;  
  case '?':  
   err_quit("unrecognized option: -%c", optopt);  
  }  
 }  
 if (debug == 0)  
  daemon_init();  
 loop();  /* never returns */  
}  
程序15.28  main函数  
loop函数是服务器的无限循环。我们将示出该函数的两种版本。程序15.29是使用  
select的一种版本。(在4.3+BSD和SVR4之下工作),程序15.30是使用poll(用于  
SVR4)的另一种版本。  
#include "opend.h"  
#include <sys/time.h>  
void  
loop(void)  
{  
 int  i, n, maxfd, maxi, listenfd, clifd, nread;  
 char buf[MAXLINE];  
 uid_t uid;  
 fd_set rset, allset;  
 FD_ZERO(&allset);  
    /* obtain fd to listen for client requests on */  
 if ( (listenfd = serv_listen(CS_OPEN)) < 0)  
  log_sys("serv_listen error");  
 FD_SET(listenfd, &allset);  
 maxfd = listenfd;  
 maxi = -1;  
 for ( ; ; ) {  
  rset = allset;  /* rset gets modified each time around */  
  if ( (n = select(maxfd + 1, &rset, NULL, NULL, NULL)) < 0)  
   log_sys("select error");  
  if (FD_ISSET(listenfd, &rset)) {  
     /* accept new client request */  
   if ( (clifd = serv_accept(listenfd, &uid)) < 0)  
    log_sys("serv_accept error: %d", clifd);  
   i = client_add(clifd, uid);  
   FD_SET(clifd, &allset);  
   if (clifd > maxfd)  
    maxfd = clifd; /* max fd for select() */  
   if (i > maxi)  
    maxi = i;  /* max index in client[] array */  
   log_msg("new connection: uid %d, fd %d", uid, clifd);  
   continue;  
  }  
  for (i = 0; i <= maxi; i++) { /* go through client[] array */  
   if ( (clifd = client[i].fd) < 0)  
    continue;  
   if (FD_ISSET(clifd, &rset)) {  
      /* read argument buffer from client */  
    if ( (nread = read(clifd, buf, MAXLINE)) < 0)  
     log_sys("read error on fd %d", clifd);  
    else if (nread == 0) {  
     log_msg("closed: uid %d, fd %d",  
          client[i].uid, clifd);  
     client_del(clifd); /* client has closed conn */  
     FD_CLR(clifd, &allset);  
     close(clifd);  
    } else   /* process client's rquest */  
     request(buf, nread, clifd, client[i].uid);  
   }  
  }  
 }  
}  
程序15.29  使用select的loop函数  
    此函数调用serv_listen以创建服务器对于客户连接的端点。此函数的其余部  
分是一个循环,它以select调用开始。在select返回后,两个条件可能为真。  
    1.描述符listenfd可能准备好读,这意味着新客户已调用了cli_conn。为了处  
理这种情况。  我们将调用serv_accept,然后更新client数组以及与该新客户相  
关的簿记消息。(我们跟踪作为select第一个参数的最高描述符编号。我们也跟踪  
使用中的client数组的最高下标。)  
    2.一个现存的客户的连接可能准备好读。这意味这下列两事件之一:(a)该  
客户已经终止,或(b)该客户已发送一新请求。如果read返回O(文件结束),则  
可认为一客户终止。如果读返回值大于0则可判定有一新请求需处理。我们调用re  
quest处理此新的客户请求。  
    我们用allset描述符集跟踪当前使用的描述符。当新客户连至服务器时,此描  
述符集的适当位被打开。当该客户终止时,适当位就被关闭。  
    因为客户的所有描述符都是由核心自动关闭的(包括与服务器的连接),所以  
我们知道什么时候一客户终止,该终止是否自愿的。这与系统V IPC机构不同。  
使用poll函数的loop函数示于程序15.30中。  
#include "opend.h"  
#include <poll.h>  
#include <stropts.h>  
void  
loop(void)  
{  
 int    i, n, maxi, listenfd, clifd, nread;  
 char   buf[MAXLINE];  
 uid_t   uid;  
 struct pollfd *pollfd;  
 if ( (pollfd = malloc(open_max() * sizeof(struct pollfd))) == NULL)  
  err_sys("malloc error");  
    /* obtain fd to listen for client requests on */  
 if ( (listenfd = serv_listen(CS_OPEN)) < 0)  
  log_sys("serv_listen error");  
 client_add(listenfd, 0); /* we use [0] for listenfd */  
 pollfd[0].fd = listenfd;  
 pollfd[0].events = POLLIN;  
 maxi = 0;  
 for ( ; ; ) {  
  if ( (n = poll(pollfd, maxi + 1, INFTIM)) < 0)  
   log_sys("select error");  
  if (pollfd[0].revents & POLLIN) {  
     /* accept new client request */  
   if ( (clifd = serv_accept(listenfd, &uid)) < 0)  
    log_sys("serv_accept error: %d", clifd);  
   i = client_add(clifd, uid);  
   pollfd[i].fd = clifd;  
   pollfd[i].events = POLLIN;  
   if (i > maxi)  
    maxi = i;  
   log_msg("new connection: uid %d, fd %d", uid, clifd);  
  }  
  for (i = 1; i <= maxi; i++) {  
   if ( (clifd = client[i].fd) < 0)  
    continue;  
   if (pollfd[i].revents & POLLHUP)  
    goto hungup;  
   else if (pollfd[i].revents & POLLIN) {  
      /* read argument buffer from client */  
    if ( (nread = read(clifd, buf, MAXLINE)) < 0)  
     log_sys("read error on fd %d", clifd);  
    else if (nread == 0) {  
 hungup:  
     log_msg("closed: uid %d, fd %d",  
          client[i].uid, clifd);  
     client_del(clifd); /* client has closed conn */  
     pollfd[i].fd = -1;  
     close(clifd);  
    } else   /* process client's rquest */  
     request(buf, nread, clifd, client[i].uid);  
   }  
  }  
 }  
}  
程序15.30  用poll的loop函数  
    为使打开描述符的数量能与客户数量相当,我们动态地为pollfd结构分配空间  
。(函数open_max见程序2.3)  
    client数组的第0个登记项用于listenfd描述符。于是,在client数组中的客  
户下标号与在pollfd数组中所用的下标号相同。新客户连接的到达由listenfd描述  
符中的POLLIN指示。如同前述,调用serv_accept以接收该连接。  
    对于一个现存的客户,应当处理来自poll的两个不同事件:一个客户终止由P  
OLLHUP指示,以及来自一现存客户的一个新要求由POLLIN指示。请回忆练习14.7,  
在还有数据在流首可读时,挂起消息可能到达流首。对于管道,在处理挂起前,我  
们希望先读所有数据。但是对于服务器,当从客户接收到挂起消息时,我们能clo  
se(关闭)该流连接,于是也就丢弃了仍在流上的所有数据。因为已经不能回送任何  
响应,所以也就没有理由再去处理仍在流上的任何请求。  
如同本函数的select版本,调用request函数(程序15.13)处理来自客户的新请求  
。此函数类似于其早期版本(程序15.16)。它调用同一函数buf_args(程序15.1  
7),buf_args  又调用cli_args(程序15.18)  
#include "opend.h"  
#include <fcntl.h>  
void  
request(char *buf, int nread, int clifd, uid_t uid)  
{  
 int  newfd;  
 if (buf[nread-1] != 0) {  
  sprintf(errmsg, "request from uid %d not null terminated: %*.*s\n",  
         uid, nread, nread, buf);  
  send_err(clifd, -1, errmsg);  
  return;  
 }  
 log_msg("request: %s, from uid %d", buf, uid);  
   /* parse the arguments, set options */  
 if (buf_args(buf, cli_args) < 0) {  
  send_err(clifd, -1, errmsg);  
  log_msg(errmsg);  
  return;  
 }  
 if ( (newfd = open(pathname, oflag)) < 0) {  
  sprintf(errmsg, "can't open %s: %s\n",  
        pathname, strerror(errno));  
  send_err(clifd, -1, errmsg);  
  log_msg(errmsg);  
  return;  
 }  
   /* send the descriptor */  
 if (send_fd(clifd, newfd) < 0)  
  log_sys("send_fd error");  
 log_msg("sent fd %d over fd %d for %s", newfd, clifd, pathname);  
 close(newfd);  /* we're done with descriptor */  
}  
程序15.31  request函数  
这样就完成了open服务器,它使用一个精灵进程处理所有的客户请求。  
习题  
15.1 改写程序15.1,要求是:对于流管道使用标准I/O库函数代替read和writ  
e。  
15.2 使用本章说明的文件描述符传送函数以及8.8节中说明的父-子进程同步例  
程,编写具有下列功能的程序。该程序调用fork,然后子进程打开一现存文件并将  
打开的描述符传给父进程。父进程读该文件的当前位移量,并打印它以便验证。若  
此文件如上述从子进程传递到父进程,则父、子进程应共享同一文件表项,所以当  
子进程每次更改该文件当前位移量,那么这种更改同样影响到父进程的描述符。使  
子进程将该文件定位至一个不同位移量,并通知父进程。  
15.3 在程序15.14和15.15中,我们分别定义和说明了全局变量,两者的区别是  
什么?  
 
15.4 改写bug_args函数(程序15.17),删除其中对argv数组长度的编译时限  
制。请用动态存储分配。  
15.5 说明优化程序15.29和程序15.30中loop函数的方法,并实现之。  
   
--  
 
 
-- 
※ 来源:·BBS 水木清华站 smth.org·[FROM: 202.38.248.38] 

BBS水木清华站∶精华区