I/O模型
首先我们将查看UNIX下可用的5种I/O模型的基本区别:
1.阻塞式I/O
2.非阻塞式I/O
3.I/O复用(select和poll)
4.信号驱动式I/O(SIGIO)
5.异步I/O(POSIX的aio_系列函数)
阻塞式I/O模型
最流行的I/O模型是阻塞式I/O模型,下面以数据报套接字作为例子,有如下的情形
非阻塞式I/O模型
进程把一个套接字设置成非阻塞式通知内核:当锁请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。
I/O复用模型
我们可以调用select和poll,阻塞在这两个系统调用中的某一个之上,而不是在真正的I/O系统调用上
下面小节将详细讲这两个函数的用法
信号驱动式I/O模型
我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。
在信号发生之后,在信号处理函数中调用recvfrom读取数据报。
异步I/O模型
信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
select函数
之前apue的学习笔记也有详细的用法 http://www.cnblogs.com/runnyu/p/4645754.html
#include <sys/select.h> int select(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,struct timeval *restrict tvptr);
参数tvptr指定愿意等待的时间,有下面3种情况
1.tvptr==NULL 永远等待,直到所指定的描述符中的一个已经准备好或捕捉到一个信号返回。如果捕捉到一个信号,则select返回-1,errno设置为EINTR
2.tvptr->tv_sec==0 && tvptr->tv_usec==0 不等待,测试所有指定的描述符并立即返回
3.tvptr->tv_sec!=0 || tvptr->tv_usec!=0 等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时立即返回
中间3个参数readfds、writefds和exceptfds是指向描述符集的指针。这3个描述符集说明了我们关心的可读、可写和处于异常条件的描述符集合。
对于描述符集fd_set结构,提供了下面4个函数
#include <sys/select.h> int FD_ISSET(int fd,fd_set *fdset); void FD_CLR(int fd,fd_set *fdset); void FD_SET(int fd,fd_set *fdset); void FD_ZERO(fd_set *fdset);
select第一个参数maxfdp1的意思是“最大文件描述符编号值加1”,在上面3个描述符集中找到最大描述符编号值,然后加1就是第一个参数值。
select有3个可能的返回值
1.返回值-1表示出错。如果在所指定的描述符一个都没准备好时捕捉到一个信号,则返回-1
2.返回0表示没有描述符准备好,指定的时间就过了。
3.一个正返回值说明了已经准备好的描述符数,在这种情况下,3个描述符集中依旧打开的位对应于已准备好的描述符。
使用select修订str_cli函数
新版本改为阻塞于select调用,或是等待标准输入可读,或是等待套接字可读。
代码如下
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include "unp.h" 2 3 void 4 str_cli(FILE *fp, int sockfd) 5 { 6 int maxfdp1; 7 fd_set rset; 8 char sendline[MAXLINE], recvline[MAXLINE]; 9 10 FD_ZERO(&rset); 11 for ( ; ; ) { 12 FD_SET(fileno(fp), &rset); 13 FD_SET(sockfd, &rset); 14 maxfdp1 = max(fileno(fp), sockfd) + 1; 15 Select(maxfdp1, &rset, NULL, NULL, NULL); 16 17 if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ 18 if (Readline(sockfd, recvline, MAXLINE) == 0) 19 err_quit("str_cli: server terminated prematurely"); 20 Fputs(recvline, stdout); 21 } 22 23 if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ 24 if (Fgets(sendline, MAXLINE, fp) == NULL) 25 return; /* all done */ 26 Writen(sockfd, sendline, strlen(sendline)); 27 } 28 } 29 }
批量输入
不幸的是,我们的str_cli函数仍然不正确。考虑下面的情况:
我们在发送数据给服务器的时候,应该等待应答。这段时间是往返时间加上服务器的处理时间。
在批量发送数据的时候,当写完最后一个请求后,我们并不能立即关闭连接,因为在客户端跟服务器之间的管道中还有其他的请求和应答。
再看一下我们的str_cli函数:当我们在标准输入键入EOF的时候,str_cli函数就此返回到main函数,随后main函数将终止。
因此标准输入中的EOF并不意味着我们同时也完成了从套接字的读入:可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。
我们需要的是一种关闭TCP连接其中一半的方法。这将由下一节shutdown函数来完成。
shutdown函数
#inlcude <sys/socket.h> int shutdown(int sockfd,int howto) //返回:若成功则为0,若出错则为-1
该函数的行为依赖于howto参数的值
SHUT_RD 关闭连接的读这一半。套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。
SHUT_WR 关闭连接的写的这一半。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。
SHUT_RDWR 连接的读半部和写半部都被关闭。
str_cli函数(再修订版)
使用shutdown来允许我们正确地处理批量输入。这个版本也废弃了以文本行为中心的代码,从而消除因为缓冲带来的复杂性。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include "unp.h" 2 3 void 4 str_cli(FILE *fp, int sockfd) 5 { 6 int maxfdp1, stdineof; 7 fd_set rset; 8 char buf[MAXLINE]; 9 int n; 10 11 stdineof = 0; 12 FD_ZERO(&rset); 13 for ( ; ; ) { 14 if (stdineof == 0) 15 FD_SET(fileno(fp), &rset); 16 FD_SET(sockfd, &rset); 17 maxfdp1 = max(fileno(fp), sockfd) + 1; 18 Select(maxfdp1, &rset, NULL, NULL, NULL); 19 20 if (FD_ISSET(sockfd, &rset)) { /* socket is readable */ 21 if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { 22 if (stdineof == 1) 23 return; /* normal termination */ 24 else 25 err_quit("str_cli: server terminated prematurely"); 26 } 27 28 Write(fileno(stdout), buf, n); 29 } 30 31 if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ 32 if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) { 33 stdineof = 1; 34 Shutdown(sockfd, SHUT_WR); /* send FIN */ 35 FD_CLR(fileno(fp), &rset); 36 continue; 37 } 38 39 Writen(sockfd, buf, n); 40 } 41 } 42 }
stdineof是一个初始化为0的新标志:只要该标志位0,每次在主循环中我们总是select标准输入的可读性。
当我们在标准输入上碰到EOF时,我们把新标志stdineof置为1,调用shutdown以发送FIN,在rset中清除标准输入。
当我们在套接字上读到EOF时,如果我们已在标准输入上遇到EOF(stdineof==1),那就是正常的终止,于是函数返回。
TCP回射服务器程序(修订版)
回顾第五章讲解的TCP回射服务器陈谷,把它重写成使用select来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* include fig01 */ 2 #include "unp.h" 3 4 int 5 main(int argc, char **argv) 6 { 7 int i, maxi, maxfd, listenfd, connfd, sockfd; 8 int nready, client[FD_SETSIZE]; 9 ssize_t n; 10 fd_set rset, allset; 11 char buf[MAXLINE]; 12 socklen_t clilen; 13 struct sockaddr_in cliaddr, servaddr; 14 15 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 16 17 bzero(&servaddr, sizeof(servaddr)); 18 servaddr.sin_family = AF_INET; 19 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 20 servaddr.sin_port = htons(SERV_PORT); 21 22 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 23 24 Listen(listenfd, LISTENQ); 25 26 maxfd = listenfd; /* initialize */ 27 maxi = -1; /* index into client[] array */ 28 for (i = 0; i < FD_SETSIZE; i++) 29 client[i] = -1; /* -1 indicates available entry */ 30 FD_ZERO(&allset); 31 FD_SET(listenfd, &allset); 32 /* end fig01 */ 33 34 /* include fig02 */ 35 for ( ; ; ) { 36 rset = allset; /* structure assignment */ 37 nready = Select(maxfd+1, &rset, NULL, NULL, NULL); 38 39 if (FD_ISSET(listenfd, &rset)) { /* new client connection */ 40 clilen = sizeof(cliaddr); 41 connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); 42 #ifdef NOTDEF 43 printf("new client: %s, port %d\n", 44 Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL), 45 ntohs(cliaddr.sin_port)); 46 #endif 47 48 for (i = 0; i < FD_SETSIZE; i++) 49 if (client[i] < 0) { 50 client[i] = connfd; /* save descriptor */ 51 break; 52 } 53 if (i == FD_SETSIZE) 54 err_quit("too many clients"); 55 56 FD_SET(connfd, &allset); /* add new descriptor to set */ 57 if (connfd > maxfd) 58 maxfd = connfd; /* for select */ 59 if (i > maxi) 60 maxi = i; /* max index in client[] array */ 61 62 if (--nready <= 0) 63 continue; /* no more readable descriptors */ 64 } 65 66 for (i = 0; i <= maxi; i++) { /* check all clients for data */ 67 if ( (sockfd = client[i]) < 0) 68 continue; 69 if (FD_ISSET(sockfd, &rset)) { 70 if ( (n = Read(sockfd, buf, MAXLINE)) == 0) { 71 /*4connection closed by client */ 72 Close(sockfd); 73 FD_CLR(sockfd, &allset); 74 client[i] = -1; 75 } else 76 Writen(sockfd, buf, n); 77 78 if (--nready <= 0) 79 break; /* no more readable descriptors */ 80 } 81 } 82 } 83 } 84 /* end fig02 */
利用select来等待某个事件发生:新客户连接的建立,数据、FIN或RST的到达。服务器将维护client数组来标识accept返回的套接字描述符。
pselect函数
POSIX.1也定义了一个select的变体,称为pselect
#include <sys/select.h> int pselect(int maxfdp1,fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,const struct timespec *restrict tsptr,const sigset_t *restrict sigmask);
除了下列几点外,pselect与select相同
1.超时值使用的结构不一样,pselect使用的是timespec结构
2.pselect的超时值被声明为const,保证了调用pselect不会改变此值
3.pselect可使用可选信号屏蔽字,在调用pselect时,以原子操作的方式安装该信号屏蔽字。在返回时,恢复以前的信号屏蔽字。
poll函数
#include <poll.h> int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
与select不同,poll构造一个pollfd结构的数组,每个数组元素指定一个描述符编号以及我们对描述符感兴趣的条件。
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
fdarray数组中的元素数由nfds指定
要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。
结构中的events/revents成员应设置为下图所示值的一个或几个
timeout参数指定poll函数返回前等待多长时间。它是一个指定应等待毫秒值的正值
TCP回射服务器程序(再修订版)
我们使用poll代替select重写上面的TCP回射服务器程序。改用poll后,我们只需分配一个pollfd结构的数组来维护客户信息,而不必分配另一个数组。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* include fig01 */ 2 #include "unp.h" 3 #include <limits.h> /* for OPEN_MAX */ 4 5 int 6 main(int argc, char **argv) 7 { 8 int i, maxi, listenfd, connfd, sockfd; 9 int nready; 10 ssize_t n; 11 char buf[MAXLINE]; 12 socklen_t clilen; 13 struct pollfd client[OPEN_MAX]; 14 struct sockaddr_in cliaddr, servaddr; 15 16 listenfd = Socket(AF_INET, SOCK_STREAM, 0); 17 18 bzero(&servaddr, sizeof(servaddr)); 19 servaddr.sin_family = AF_INET; 20 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 21 servaddr.sin_port = htons(SERV_PORT); 22 23 Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 24 25 Listen(listenfd, LISTENQ); 26 27 client[0].fd = listenfd; 28 client[0].events = POLLRDNORM; 29 for (i = 1; i < OPEN_MAX; i++) 30 client[i].fd = -1; /* -1 indicates available entry */ 31 maxi = 0; /* max index into client[] array */ 32 /* end fig01 */ 33 34 /* include fig02 */ 35 for ( ; ; ) { 36 nready = Poll(client, maxi+1, INFTIM); 37 38 if (client[0].revents & POLLRDNORM) { /* new client connection */ 39 clilen = sizeof(cliaddr); 40 connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); 41 #ifdef NOTDEF 42 printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen)); 43 #endif 44 45 for (i = 1; i < OPEN_MAX; i++) 46 if (client[i].fd < 0) { 47 client[i].fd = connfd; /* save descriptor */ 48 break; 49 } 50 if (i == OPEN_MAX) 51 err_quit("too many clients"); 52 53 client[i].events = POLLRDNORM; 54 if (i > maxi) 55 maxi = i; /* max index in client[] array */ 56 57 if (--nready <= 0) 58 continue; /* no more readable descriptors */ 59 } 60 61 for (i = 1; i <= maxi; i++) { /* check all clients for data */ 62 if ( (sockfd = client[i].fd) < 0) 63 continue; 64 if (client[i].revents & (POLLRDNORM | POLLERR)) { 65 if ( (n = read(sockfd, buf, MAXLINE)) < 0) { 66 if (errno == ECONNRESET) { 67 /*4connection reset by client */ 68 #ifdef NOTDEF 69 printf("client[%d] aborted connection\n", i); 70 #endif 71 Close(sockfd); 72 client[i].fd = -1; 73 } else 74 err_sys("read error"); 75 } else if (n == 0) { 76 /*4connection closed by client */ 77 #ifdef NOTDEF 78 printf("client[%d] closed connection\n", i); 79 #endif 80 Close(sockfd); 81 client[i].fd = -1; 82 } else 83 Writen(sockfd, buf, n); 84 85 if (--nready <= 0) 86 break; /* no more readable descriptors */ 87 } 88 } 89 } 90 } 91 /* end fig02 */