一、概述
我们前面看到TCP客户同时处理两个输入:标准输入和TCP套接字。我们遇到的问题是客户阻塞于(标准输入)fgets调用期间,服务器进程会被kill。服务器TCP虽然正确地发送了一个FIN,但是客户进程正阻塞于标准输入的过程,它将看不到这个EOF,直到从套接字读时为止(可能已经过了很久我们才输入)。
这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定一个或者多个I/O条件就绪,它就通知进程,这个能力成为IO复用,是有select和poll这两个函数支持的。
I/O复用典型使用在下列网络应用场合:
1)当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。上一节
2)一个客户同时处理多个套接字是可能的,不过比较少见。在16.5节结合一个web客户的上下文给出这种场合使用select的例子
3)如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。本节
4)如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。8.15节有这么一个例子
5)如果一个服务器要处理多个服务或者多个协议(在13.5节讲述的inetd守护进程),就要用I/O复用
I/O复用并非只限于网络编程,许多重要的应用程序也需要使用这项技术。
二、I/O模型
五种I/O模型:
阻塞式/非阻塞式 I/O、I/O复用、信号驱动I/O、异步I/O
一个输入操作通常包括两个不同的阶段:
1)等待数据准备好。
2)从内核向进程复制数据。
对于一个套接字输入:第一步就是涉及等待数据从网络中到达,当所等待的分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制应用到进程缓冲区。
阻塞式I/O模型:(最流行)
我们以UDP为例子:
可以看到:进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回时,应用进程开始处理数据报。
2、非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。16章中详细~
前三次调用recvfrom时没有数据返回,因此内核立即返回一个EWOLDBLOCK错误。第四次调用recvfrom时已经有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回,我们继续接着处理数据。
轮询往往耗费大量CPU时间。不过这种模型偶尔也会遇到,通常是专门提供某一种功能的系统中才用。
3、I/O复用模型
有了I/O复用,我们就能调用select或者poll上,阻塞在这两个系统上调用中的某一个上,而不是阻塞在真正的I/O上。
我们阻塞与select调用,等待数据报套接字变为可读。当select返回的套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用缓冲区。
比较上面的两种阻塞/非阻塞式,I/O复用并不显得有什么优势,事实上,select的优势在于可以等待多个描述符就绪。
4、信号驱动时I/O模型
可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。
我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理。也可以立即通知循环,让它读取数据报。
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
5、异步I/O模型
告知内核启动某个动作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成 后通知我们。
这种模型与信号驱动模型的主要区别:信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O是由内核通知我们I/O操作何时完成。
I/O模型对比
同步I/O模型:导致请求进程阻塞,直到I/O操作完成。
异步I/O模型:不导致请求进程阻塞。
前4中模型都是同步进程:真正的I/O操作将阻塞进程。
三、select函数
select函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或者多个事件发生或经过某个指定的时间后才唤醒进程。也就是说,我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。我们所关心的描述字不受限于套接口,任何描述符都可用select来监听。
最后一个参数内核等待指定描述符的任何一个就绪花的时间。
三种可能:1、永远等待 (设置为空指针) 2、等待一段固定 3、根本不等待(设置为0),轮询。
但是这里的时间是要被操作系统给弄得很粗糙,向上舍入10ms的倍数。
timeout参数是const的不可被修改。
select使用描述符集,通常是一个数组,其中每个整数的每一位对应一个描述符。比如:32位整数,该数组的第一个元素对应于一个描述符0~31,第二个元素对应于描述符32~63,依次类推,所有这些实现都与程序无关,他们隐藏在名为fd_set的数据类型和以下四个宏中:
描述符就绪条件:
四、str_cli函数
使用select重写了上一节的str_cli函数,本版本阻塞与select调用:
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);//init
for ( ; ; ) {
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;
Select(maxfdp1, &rset, NULL, NULL, NULL);//写,异常,timeout均为空(一直阻塞)
if (FD_ISSET(sockfd, &rset)) { /* socket is readable */
if (Readline(sockfd, recvline, MAXLINE) == 0)
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)
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
依然存在问题:
在批量输入的情况下,我们写完不能立即关闭连接,因为管道中存在其他的请求和应答。问题在于我们对标准输入的EOF的处理:str_cli函数就此返回到main函数,main函数随后终止,标准输入的EOF并不意味着我们同时也完成了从套接字的读入;可能仍有请求在去往服务器的路上,或者仍有应答返回客户的路上。
缓冲机制:fgets读取输入,数据被转入到缓冲区中。然而fgets只返回第一行,其余输入行仍在stdio缓冲区中。select中不知道stdio使用了缓冲区,它只是从read的系统调用角度指出是否有数据可读,而不是从fgets之类的角度来考虑。所以不要混用select和stdio。
五、shutdown函数
终止网络连接的方法通常是调用close函数,不过它有两个限制:
1)close把描述符引用计数-1,仅在该计数变为0时才关闭该套接字。使用shutdown可也不管引用计数就激发TCP的正常终止序列。
2)close终止读和写两个方向的数据传送。既然TCP是全双工的,我们就可以仅关闭一端的读和写。
所以有了str_cli的改进版本:
#include "unp.h"
void
str_cli(FILE *fp, int sockfd)
{
int maxfdp1, stdineof = 0;//0:正常 1:已经关闭
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
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 termination */
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));
}
}
}
六、TCP回射服务器程序
前面为每个客户派生一个子进程,这里可以使用select来处理任何个客户的单进程程序。
用于跟踪客户的数据结构:
/* include fig01 */
#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 buf[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; /* initialize */
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);
/* end fig01 */
/* include fig02 */
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);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
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 = Read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */
七、poll函数
poll函数和select函数类似
/* include fig02 */
for ( ; ; ) {
nready = Poll(client, maxi+1, INFTIM);
if (client[0].revents & POLLRDNORM) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif
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 = read(sockfd, buf, MAXLINE)) < 0) {
if (errno == ECONNRESET) {
/*4connection reset by client */
#ifdef NOTDEF
printf("client[%d] aborted connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
err_sys("read error");
} else if (n == 0) {
/*4connection closed by client */
#ifdef NOTDEF
printf("client[%d] closed connection\n", i);
#endif
Close(sockfd);
client[i].fd = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */