UNP学习笔记(第六章 I/O复用)

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调用,或是等待标准输入可读,或是等待套接字可读。

代码如下

 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 }
View Code

批量输入

不幸的是,我们的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来允许我们正确地处理批量输入。这个版本也废弃了以文本行为中心的代码,从而消除因为缓冲带来的复杂性。

 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 }
View Code

stdineof是一个初始化为0的新标志:只要该标志位0,每次在主循环中我们总是select标准输入的可读性。

当我们在标准输入上碰到EOF时,我们把新标志stdineof置为1,调用shutdown以发送FIN,在rset中清除标准输入。

当我们在套接字上读到EOF时,如果我们已在标准输入上遇到EOF(stdineof==1),那就是正常的终止,于是函数返回。

TCP回射服务器程序(修订版)

回顾第五章讲解的TCP回射服务器陈谷,把它重写成使用select来处理任意个客户的单进程程序,而不是为每个客户派生一个子进程。

 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
",
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 */
View Code

利用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结构的数组来维护客户信息,而不必分配另一个数组。

 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
", 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
", 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
", 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 */
View Code
原文地址:https://www.cnblogs.com/runnyu/p/4660680.html