UNP Chapter 6 - I/O复用: select和poll函数
6.1.概述
我们需要这样的能力:如果一个或多个I/O条件满足时,例如,输入已经准备好被读,或者描述字可以承接更多的输出,我们就被通知到。这个能力称为I/O复用,是由函数select和poll支持的。I/O复用典型地用在下列网络应用场合:
1.当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。
2.一个客户同时处理多个套接口是可能的,但是很少出现。
3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
4. 如果一个服务器既要处理TCP,又要处理UDP,一般也要使用I/O复用。
5. 如果一个服务器要处理多个服务或者多个协议,一般要使用I/O复用。
6.2. I/O模型
在介绍函数select和poll之前,我们需要回头来看看整体,检查Unix下我们可用的五个I/O模型的基本区别:
1. 阻塞I/O
2. 非阻塞I/O
3. I/O复用(select和poll)
4. 信号驱动I/O(SIGIO)
5. 异步I/O
一个输入操作一般有两个不同的阶段:
1. 等待数据准备好
2. 从内核到进程拷贝数据。
对于一个套接口上的输入操作,第一步一般是等待数据到达网络,当分组到达时,它被拷贝到内核中的某个缓冲区,第二步是将数据从内核缓冲区拷贝到应用缓冲区。
1.阻塞I/O模型
最流行的I/O模型是阻塞I/O模型,缺省时,所有套接口都是阻塞的。
2. 非阻塞I/O模型
前三次调用recvfrom时仍无数据返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时,数据已经准备好,被拷贝到应用缓冲区,recvfrom返回成功指示,接着就是我们处理数据。当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称此过程为轮询(polling)。应用进程连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,但这种模型只是偶尔才遇到,一般是在只专门提供某种功能的系统中才有。
3. I/O复用模型
有了I/O复用,我们就可以调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正的I/O系统调用。如图6.3,我们阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,我们调用recvfrom将数据报拷贝到应用缓冲区中。使用select的好处在于我们可以等待多个描述字准备好。
4. 信号驱动I/O模型
我们也可以用信号,让内核在描述字准备好时,用信号SIGIO通知我们,我们将此方法称为信号驱动I/O,如图6.4
首先我们允许套接口进行信号驱动I/O,并通过系统调用sigaction安装一个信号处理程序。此系统调用立即返回,进程继续工作,它是非阻塞的。当数据报准备好被读时,就为该进程生成一个SIGIO信号。我们随即可以在信号处理程序中调用recvfrom来读数据报,并通知主循环数据已准备好被处理。也可以通知主循环,让它来读数据报。无论我们如何处理SIGIO信号,这种模型的好处是当等待数据报到达时,可以不阻塞。主循环可以继续执行,只是等待信号处理程序的通知: 或者数据已准备好处理,或者数据报已准备好被读。
5. 异步I/O模型
异步I/O是POSIX实时扩展,我们让内核启动操作,并在整个操作完成后(包括将数据从内核拷贝到我们自己的缓冲区)通知我们。这种模型没有广泛使用。这种模型与前一节介绍的信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。我们调用函数aio_read(POSIX异步I/O函数以aio_或者lio_开头),给内核传递描述字,缓冲区指针,缓冲区大小(与read相同的三个参数),文件偏移(与lseek类似),并告诉内核当前整个操作完成是如何通知我们。此系统调用立即返回,我们的进程不阻塞于等待I/O操作的完成。在此例子中,我们假设要求内核在操作完成时生成一个信号,此信号直到数据已拷贝到应用缓冲区才生成,这一点是与信号驱动I/O模型不同的。
6. 五种不同I/O模型的比较
7. 同步I/O与异步I/O
POSIX定义这两个术语如下:
同步I/O操作引起请求进程阻塞,知道I/O操作完成。
异步I/O操作不引起请求进程阻塞。
根据上述定义,我们的前四个模型--阻塞I/O模型,非阻塞I/O模型,I/O复用模型和信号驱动I/O模型都是同步I/O模型,因为真正的I/O操作(recvfrom)阻塞进程,只有异步I/O模型与异步I/O的定义相匹配。
6.3. select函数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
// 返回: 准备好描述字的正数目, 0 -超时, -1 -出错
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。
我们从此函数的最后一个参数开始介绍,它告诉内核等待一组指定的描述字中的任一个准备好可花多长时间,结果timeval指定了秒数和微秒数成员
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
有三种可能:
1. 永远等下去:仅在有一个描述字准备好I/O时才返回,为此,我们将参数timeout设置为空指针。
2. 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数。
3. 根本不等待:检查描述字后立即返回,这称为轮询(polling)。为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0
在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般被中断。
中间三个参数readset,wirteset和exceptset指定我们要让内核测试读写和异常条件所需的描述字。
函数select使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。
// 四个宏
void FD_ZERO(fd_set * fdset); /* clear all bits in fdset */
void FD_SET(int fd, fd_set * fdset); /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set * fdset); /* turn on the bit for fd in fdset */
int FD_ISSET(int fd, fd_set * fdset); /* is the bit for fd on in fdset */
例如:为了定义一个fd_set类型的变量,并打开描述字1,4和5的相应位,我们写如下代码
fd_set rset;
FD_ZERO(&rset); /* initiallize the set, all bits off */
FD_SET(1, &rset); /* turn on bit for fd 1 */
FD_SET(4, &rset); /* turn on bit for fd 4 */
FD_SET(5, &rset); /* turn on bit for fd 5 */
对集合的初始化是很重要的,如果集合作为一个自动变量分配而未初始化,那将导致不可预测的后果。
参数maxfdp1指定被测试的描述字个数,它的值是要被测试的最大描述字加1
6.4. str_cli函数(Revisited)
6.5. 批量输入
6.6. shutdown函数
终止网络连接的正常方法是调用close,但close有两个限制可由函数shutdown来避免:
1. close将描述字的访问计数减1,仅在此计数为0时才关闭套接口。用shutdown我们可以激发TCP的正常连接终止序列,而不管访问计数。
2. close终止了数据传送的两个方向:读和写。由于TCP连接是全双工的,有很多时候我们要通知另一端我们已经完成了数据发送,即使那一端仍有许多数据要发送也是如此。
#include <sys/socket.h>
int shutdown(int sockfd, int howto); // 返回: 0-成功, -1-出错
The action of the function depends on the value of the howto argument.
SHUT_RD : The read half of the connection is closed.
SHUT_WR : the write half of the connection is closed.
SHUT_RDWR: the read half and the write half of the connection are both closed
6.7. str_cli函数(Revisited Again)
#include "unp.h"
void str_cli(FILE * fp, int sockfd)
{
int maxfdp1, stdineof;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
stdineof = 0;
FD_ZERO(&rset);
for ( ; ; )
{
if(stdineof == 0)
FD_SET(fileno(fp),&rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp),sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rset)) // socket is readable
{
if(Readline(sockfd, recvline, MAXLINE) == 0)
{
if(stdineof == 1)
return; // normal terminal
else
err_quit("str_cli: server terminated prematurely");
}
Fputs(recvline,stdout);
}
if(FD_ISSET(fileno(fp), &rset)) // input is readable
{
if(Fgets(sendline, MAXLINE, fp) == NULL)
{
stdineof = 1;
Shutdown(sockfd, SHUT_WR); // send FIN
FD_CLR(fileno(fp),&rset);
continue;
}
Writen(sockfd, sendline, strlen(sendline));
}
}
}
6.8. TCP回射服务器程序(Revisited)
#include "unp.h"
int main(int argc, char * * argv)
{
int i,maxi,maxfd,listenfd,connfd,sockfd;
int nready,client[FD_SETSIZE];
ssize_t n;
fd_set rset,allset;
char line[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
maxfd = listenfd; /* initiallize */
maxi = -1; /* index into client[] array */
for( i = 0; i < FD_SETSIZE; I++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; )
{
rset = allset; /* structure assignment */
nready = Select()maxfd+1, &rset, NULL, NULL, NULL;
if(FD_ISSET(listenfd, &rset)) /* new client connection */
{
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);
for(i=0; i < FD_SETSIZE; i++)
{
if(client[i]<0)
{
client[i] = connfd; /*save descriptor */
break;
}
}
if(i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset); /* add new descriptor to set */
if(connfd > maxfd)
maxfd = connfd; /* for select */
if(i > maxi)
maxi = i; /* max index in client[] array */
if(--nready <= 0)
continue; /* no more readable descriptors */
}
for(i=0; i<= maxi; i++) /* check all clients for data */
{
if((sockfd = client[i]) < 0)
continue;
if(FD_ISSET(sockfd, &rset))
{
if((n=Readline(sockfd, line, MAXLINE)) == 0)
{ /* connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
}
else
{
Written(sockfd, line ,n);
}
if(--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
很不幸,上面的服务器程序有一个问题。考虑一下,如果一个客户连接到服务器上,发送一个字节的数据(而不是一行)后就睡眠,服务器将调用readline,它从客户上读到单个字节的数据,然后就阻塞与下一个read调用以等待此客户的其他数据。接着,服务器就阻塞于(“挂起”可能是一个更好的说法)此单个客户,不能为任何其他客户提供服务(不论是新的客户连接还是现有客户的数据)。这种状况一直要持续到此客户发出一个换行符或者终止为止。
这里的一个基本概念就是当一个服务器正在处理多个客户时,服务器决不能阻塞于只与单个客户相关的函数调用。如果这样的话,服务器将悬挂并拒绝为其他客户服务,这称为拒绝服务型攻击(denial of service). 他对服务器做了某些动作后,服务器就不能为其他合法用户服务了。可能的解决办法是: (a) 使用非阻塞I/O模型, (b)让每个客户由单独的控制线程提供服务(例如创建子进程或线程来为每个客户提供服务),(c)对I/O操作设置超时。
6.9. pselect函数
#include <sys/select.h>
#include <signal.h>
#include <time.h>
int pselect(int maxfdp1, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timespec * timeout, const sygset_t * sigmask);
// 返回: 准备好描述字的个数, 0-超时 -1-出错
其中timespec的tv_nsec规定纳秒数
struct timespec
{
time_t tv_sec; // seconds
long tv_nsec; // nanoseconds
};
其中第六个参数:指向信号掩码的指针。
6.10. poll函数
poll提供了与select相似的功能,但当涉及到流设备时,它还提供了附加信息。
#include <poll.h>
int poll(struct pollfd * fdarray, unsigned long nfds, int timeout); // 返回: 准备好描述字的个数, 0-超时, -1-出错
第一个参数是指向一个结构数组第一个元素的指针,每个数组元素都是一个pollfd结构,它规定了为测试一给定描述字fd的一些条件。
struct pollfd { int fd; /* descriptor to check */ short events /* events of interest on fd */ short revents /* events that occurred on fd */ };
poll识别三个类别的数据:普通(normal),优先级带(priority band),高优先级(high priority),这些术语均出自基于流的实现。
6.11. TCP回射服务器程序(Revisited Again)
现在我们用poll而不是select来重写TCP回射服务器程序。
#include "unp.h"
#include <limits.h> /* for OPEN_MAX */
int main(int argc, char * * argv)
{
int i, maxi, listenfd, connfd, sockfd;
int nready;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i = 1; i < OPEN_MAX; i++)
client[i].fd = -1; /* -1 indicates available entry */
maxi = 0; /* max index into client[] array */
for( ; ; )
{
nready = Poll(client, maxi + 1, INFTIM);
if(client[0].revents & POLLRDNORM) /* new client connection
{
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA*)&cliaddr, &clilen);
for(i = 1; i < OPEN_MAX; i++)
{
if(client[i].fd < 0)
{
client[i].fd = connfd; /* save descriptor */
break;
}
}
if(i == OPEN_MAX)
err_quit("too many clients");
client[i].events = POLLRDNORM;
if( i > maxi)
maxi = i; /* max index in client[] array */
if( --nready < = 0)
continue; /* no more readable descriptors */
}
for( i = 1; i <= maxi; i++) /* check all clients for data */
{
if( (sockfd = client[i].fd) < 0)
continue;
if(client[i].revents & (POLLRDNORM | POLLERR))
{
if( (n = readline(sockfd, line, MAXLINE)) < 0)
{
if(errno == ECONNRESET) /* connection reset by client */
{
Close(sockfd);
clilent[i].fd = -1;
}
else
err_sys("readline error");
}
else if(n == 0) /* connection closed by client */
{
Close(sockfd);
client[i].fd = -1;
}
else
Writen(sockfd, line, n);
if(--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
6.12. 小结