第十五章 高级进程间通信
时间:2005-08-31 来源:mantou
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
#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 ");
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
#include
#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
#include
#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
#include
#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
#include /* struct msghdr */
#include /* struct iovec */
#include
#include
#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
#include /* struct msghdr */
#include /* struct iovec */
#include
#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
#include /* struct msghdr */
#include /* struct iovec */
#include
#include
#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
#include /* struct msghdr */
#include /* struct iovec */
#include
#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 o
是open函数的第二个参数,以十进制表示。该请求字符串以null字节结
尾。
2. 服务器调用send_fd 或send_err回送一打开描述符或一条出错消息。这是一个
进程向其父进程发送一打开描述符的实例。在15.6节,我们将修改此实例,其中使
用了一个精灵服务器,它将一个描述符发送给完全无关的进程。
程序15.11是头文件open.h,它包括标准系统头文件,并且定义了各个函数原
型。
#include
#include
。文件内容则不需用IPC传送。
将服务器设计成一个单独的可执行程序有很多优点:
1. 任一客户都易于和服务器联系,这类似于客户调用一库函数。我们不需要将一
特定服务编码在应用程序中,而是设计一种可供重用的设施。
2. 如若需要更改服务器,那么也只影响一道程序。相反,更新一库函数可能要更
改调用此库函数的所有程序(用连编程序重新连接)。共享库函数可以简化这种更
新。
3. 服务器可以是设置__用户__ID程序,于是使其具有客户没有的附加许可权。注
意,一个库函数(或共享库函数)不能提供这种能力。
客户进程创建一流管道,然后调用fork和exec以调用服务器。客户经流管道发
送请求,服务器经管道回送响应。我们定义客户和服务器间的协议如下。
1. 客户经流管道向服务器发送下列形式的请求:
open o
是open函数的第二个参数,以十进制表示。该请求字符串以null字节结
尾。
2. 服务器调用send_fd 或send_err回送一打开描述符或一条出错消息。这是一个
进程向其父进程发送一打开描述符的实例。在15.6节,我们将修改此实例,其中使
用了一个精灵服务器,它将一个描述符发送给完全无关的进程。
程序15.11是头文件open.h,它包括标准系统头文件,并且定义了各个函数原
型。
#include
#include
#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
#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 /* 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
#include
#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
void
request(char *buf, int nread, int fd)
{
int newfd;
if (buf[nread-1] != 0) {
sprintf(errmsg, "request not null terminated: %*.*s ",
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 ",
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 " " /* 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: ");
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
#include
#include
#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, &