apue和unp的学习之旅08——I/O复用

来源:互联网 发布:淘宝店铺卖家评价大全 编辑:程序博客网 时间:2024/05/09 18:45

//------------------------------1.Unix下可用的5种I/O模型的基本区别----------------------------------

POSIX有定义,同步IO操作(synchronous I/O Operation)导致请求进程阻塞,直到I/O操作完成。

异步I/O操作(asynchrounous I/O operation)不导致请求进程阻塞。



根据上述定义,前4种模型都是同步I/O模型,因为其中真正的I/O操作(recvfrom)都将阻塞进程,只有异步模型与POSIX定义的异步I/O相匹配。


//-----------------------------------2.I/O复用使用的场合------------------------------------

1.当客户处理多个描述符(通常是交互式输入和网络套接字时)必须使用I/O复用。

2.一个客户同时处理多个套接字也是可能的,但比较少见。

3.如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。

4.如果一个服务器既要处理TCP又要处理UDP,一般就要使用I/O复用。

5.如果一个服务器要处理多个服务或则多个协议,一般就要使用I/O复用。


//------------------------------------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);


1.timeval结构

struct timeval {

    long tv_sec;     // seconds

    long tv_usec;    // microseconds

};

该参数有三种可能,

1个可能是设置为空指针,即仅在有一个描述符准备好I/O时才返回,不然永远等待下去。

2个可能是等待一段固定时间,在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的timeval 结构中指定的秒数和微秒数。

3.不等待,检查描述符后立即返回,这称为轮询(polling),为此,该参数必须指向一个timeval结构,而且其中的字段都必须是0

前2种的往往有可能进程等待期间被被捕获的信号中断,并从信号处理函数返回。不同系统对被中断的select的是否自动重启不同,因此为了可移植性考虑,如果我们在捕获信号,那么必须做好select返回EINTR错误的准备。


尽管timeval结构允许我们指定一个微妙级的分辨率,但是内核支持的真实分辨率往往粗糙的多,例如许多unix把超时值向上舍入成10ms的倍数,另外还涉及调度的延迟,也就是说定时器时间到后,内核还需要花一点时间调度相应进程运行。timeval结构能够表达select不支持的值,比如tv_sec为1亿秒,但是select函数会以EINVAL错误返回。


有些linux版本会修改这个timeval结构,因此从可移植性考虑,应该假设该timeval结构在select返回时未被定义,因而每次调用select之前都得对它进行重新初始化,不过幸好,POSIX规定对该结构使用const限定词。


再来说说中间的3个参数readset,writeset,和exceptcet指定我们要让内核测试读,写和异常条件的描述符。目前支持的异常条件有2种:1是某个套接字的带外数据的到达,2是某个已置为分组模式的伪终端存在课从主端读取的控制状态信息。


描述符集的初始化非常重要,因为作为自动变量分配的一个描述符集若没初始化,那么可能发生不可预期的后果。

select函数的中间三个参数readset,writeset,exceptset中,如果我们对某个条件不感兴趣,就可以把它设计为空指针。事实上,如果三个指针都为空,我们就有了一个比unix的sleep函数更为精确的定时器(sleep以秒为最小单位),poll函数也可以类似的功能。


maxfdp1参数指定待测试的描述符的个数,它的值是待测试的描述符+1,描述符0,1,2,...maxfdp1-1均会被测试。


头文件<sys/select.h>中定义的FD_SETSIZE常值是数据类型fd_set中的描述符总数,其值通常是1024,不过很少有程序用到那么多的描述符。maxfdp1参数迫使我们计算出所关心的最大描述符,让它+1,并告知内核该值。之所以要有这个参数,纯粹是为了效率的缘故,每个fd_set都有表示大量描述符(典型数量为1024),然而一个普通进程所用数量却少的多,内核正是通过在进程与内核之间不复制描述符集中不必要的部分,从而不测试总为0的那些位来提高效率的。


select函数修改由指针readset,writeset和exceptcet所指向的描述符集,因而这三个参数都是值-结果参数。该函数返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符,描述符集内任何与未就绪描述符对应的位均清成0,为此,每次重心调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1.


该函数的返回值表示跨所有描述符集的已就绪的总位数,如果在任何描述符就绪之前定时器到时,那么返回0.返回-1表示出错(比如被一个所捕获的信号中断)。


//---------------------------------------4.描述符就绪条件------------------------------------

尽管可读性与课写性对于普通文件这样的描述符显而易见,然而对于引起select返回套接字“就绪”的条件我们必须讨论的更明确些。

1.满足下列四个条件的任何一个时,一个套接字准备好读:

a).该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其值默认是1.

b).该连接的读半部关闭(也就是接收了FIN的TCP连接),对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)

c).该套接字是一个监听套接字且已完成的连接数不为0.

d).其上有一个套接字错误待处理,对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确却的错误条件。这些待处理的错误(pengding error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

2.下列四个条件中的任何一个时,一个套接字准备好写:

a).该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记,对于TCP和UDP而言,其默认值是2048.

b).该连接的写半部关闭,对这样的套接字的写操作将产生SIGPIPE信号。

c).使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。

d).其上有一个套接字错误待处理,对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确却的错误条件,这些待处理的错误也可以通过指定SO_ERROR选项调用getsockopt获取并清除。

3.如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。

注意,当某个套接字上发生错误时,它将由select标记既可读又可写。

设计出接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。举例来说,如果我们知道除非至少存在64个字节的数据,否则我们的应用进程没有任何有效工作可言,那么可以把接收地水位标记设置为64,以防止少于64个字节准备好读时select唤醒我们。

任何UDP套接字只要发送缓冲区大小大于等于其发送低水位标记就总是可写的,这是因为UDP套接字不需要连接。

通过一个表总结下:



//-----------------------------------5.客户的str_cli函数改进版------------------------------------

早先的那个版本问题在于:当套接字上发生某些事件时,客户可能阻塞于fgets调用。

新版本改为在数据准本好前阻塞于select调用,而不是具体的套接字I/O函数或者标准I/O函数

#include"unp.h"voidstr_cli(FILE *fp, int sockfd){intmaxfdp1;fd_setrset;charsendline[MAXLINE], recvline[MAXLINE];FD_ZERO(&rset);for ( ; ; ) {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)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));}}}
//-----------------------------------6.客户的str_cli函数再次改进版--------------------------------

为什么还要改进呢,问题的起因在于我们对标准输入中的EOF的处理,str_cli函数就此返回到main函数,而main函数随后终止,然而在批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接字的读入,可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。我们需要的是一种关闭TCP连接中其中一半的方法。也就是说,我们想给服务器发送一个FIN,告诉它我们已经完成了数据发送,但是仍然保持套接字描述符打开以便读取。这就得让shutdown函数上场提供它的一部分功能了。

#include <sys/socket.h>

int  shutdown(int sockfd, int howto);

                                                                ret:若成功则为0,失败出错则为-1

终止网络连接的通常方法是调用close函数,不过close却有2个限制,都可以用shutdown来避免。

1.close把描述符减1,仅在该计数变为0时才关闭套接字,使用shutdown可以不管引用计数就激发TCP的正常连接终止系列。

2.close终止读和写2个方向的数据传送,就会导致上面提到的问题。既然TCP是全双工的,有时候我们需要告知对端我们已经完成了数据发送,即时对端仍有数据要发送给我们。


函数的行为取决于howto参数的值:

SHUT_RD:关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。

SHUT_WR:关闭连接的写这一版——当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常终止系列。我们已经说过,不管套接字描述符引用计数是否等于0,这样的写半部关闭照样执行,进程不能再对这样的套接字调用任何写函数。

SHUT_RDWR:连接的读半部和写半部都关闭——这与分别调用shutdown指定SHUT_RD,和shutdown指定SHUT_WR等效。

于是改进版的客户str_cli函数如下:

#include"unp.h"voidstr_cli(FILE *fp, int sockfd){intmaxfdp1, stdineof;fd_setrset;charbuf[MAXLINE];intn;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 ( (n = Read(sockfd, buf, MAXLINE)) == 0) {if (stdineof == 1)return;/* normal termination */elseerr_quit("str_cli: server terminated prematurely");}Write(fileno(stdout), buf, n);}if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */if ( (n = Read(fileno(fp), buf, MAXLINE)) == 0) {stdineof = 1;Shutdown(sockfd, SHUT_WR);/* send FIN */FD_CLR(fileno(fp), &rset);continue;}Writen(sockfd, buf, n);}}}

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

在标准输入上没有遇到EOF,那么是服务器进程过早终止。

而且现在改为read和write针对缓冲区操作,而不是针对文本行操作。


//-----------------------------------7.使用select的服务器----------------------------------------

#include"unp.h"intmain(int argc, char **argv){inti, maxi, maxfd, listenfd, connfd, sockfd;intnready, client[FD_SETSIZE];ssize_tn;fd_setrset, allset;charbuf[MAXLINE];socklen_tclilen;struct sockaddr_incliaddr, 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);#ifdefNOTDEFprintf("new client: %s, port %d\n",Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),ntohs(cliaddr.sin_port));#endiffor (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;} elseWriten(sockfd, buf, n);if (--nready <= 0)break;/* no more readable descriptors */}}}}

//-------------------------------------8.拒绝服务型攻击----------------------------------------

上面的select服务器程序还是存在问题,假如有一个恶意的客户连接到服务器,发送一个字节的数据(不是换行符)后进入睡眠,将会发什么?服务器将调用read,它从客户读入这个单字节的数据,然后阻塞于下一个read调用,以等待来自客户的其余数据(假设现在面向的是文本行的回射服务器,而上面的程序时针对缓冲区而文问本行的),服务器于是因为一个客户而被阻塞,不能再为其他任何客户提供服务,知道那个而已的客户发出一个换行符或者终止为止。

可能解决的办法包括如下:

a).使用非阻塞式I/O

b).让每个客户由单独的控制线程提供服务。

c).对I/O操作设置一超时。


//-----------------------------------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 stuct timespec* timeout, const sigset_t sigmask);

                                           // ret: 若有就绪描述符则为其数目,若超时则为0,若出错则为-1

struct  timespec {

   time_t  tv_sec;      // seconds

   long    tv_nsec;     // nanoseconds 纳秒

};


pselect函数增加了第6个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。


if(intr_flag)                

{

    handle_intr();

}                                                // mark1

if((nready = select(...) ) < 0 )                 // mark2

{

    if(errno == EINTR)

    {    

       if(intr_flag)

       {

          handle_intr();

       }

    }   

}

问题是,在mark1和mark2之间可能会有SIGINT信号发生,那么若select永远阻塞,该信号将丢失。但是有了pselect后,就能按照下面的方式可靠地编写这个例子了,

sigset_t   newmask,oldmask,zeromask;

sigemptyset(&zeromask);

sigempty(&newmask);

sigaddset(&newmask, SIGINT);


sigprocmask(SIG_BLOCK, &newmask, &oldmask);            // block SIGINT

if(intr_flag)

{     

    handle_intr();

}

if((nready = pselect(...,&zeromask))  < 0)

{

    if(errno == EINTR)

    {   

       if(intr_flag)

       {     

           handle_intr();

       }

    }

}

在测试intr_flag变量之前,我们阻塞SIGINT,当pselect调用时,它先以空集(zeromask)替代进程的信号掩码,再检查描述符,并可能进入睡眠。然而当pselect函数返回时,进程的信号掩码又被重置为newmask,即SIGINT被阻塞。


//-------------------------------------10.poll函数-----------------------------------------

#include <poll.h>

int poll(struct pollfd* fdarray, unsigned long nfds, timeout);

                              // ret:若有就绪描述符则为其数目,若超时则为0,若出错则为-1

 

struct pollfd 

    int fd;           // descripter to check

    short events;     // events of interest on fd

    short revents;    // events that occurred on fd

};

要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态,从而避免使用值-结果参数。


如果我们不再关心某个特定描述符,那么可以把与它对应的poolfd结构的fd成员设置成一个负值,poll函数将忽略这样的pollfd结构的events成员,返回时将它的reevnets成员的值置为0.


不像select和pselect,现在分配一个pollfd结构的数组并把数组中的元素的树木通知内核成了调用者的责任,内核不再需要知道类似fd_set的固定FD_SETSIZE大小的数据类型来维护客户信息了。

服务器使用poll的例子:

/* include fig01 */#include"unp.h"#include<limits.h>/* for OPEN_MAX */intmain(int argc, char **argv){inti, maxi, listenfd, connfd, sockfd;intnready;ssize_tn;charbuf[MAXLINE];socklen_tclilen;struct pollfdclient[OPEN_MAX];struct sockaddr_incliaddr, 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 *//* end fig01 *//* 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);#ifdefNOTDEFprintf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));#endiffor (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 */#ifdefNOTDEFprintf("client[%d] aborted connection\n", i);#endifClose(sockfd);client[i].fd = -1;} elseerr_sys("read error");} else if (n == 0) {/*4connection closed by client */#ifdefNOTDEFprintf("client[%d] closed connection\n", i);#endifClose(sockfd);client[i].fd = -1;} elseWriten(sockfd, buf, n);if (--nready <= 0)break;/* no more readable descriptors */}}}}/* end fig02 */


因为确定一个进程任何时刻能够打开的最大描述符数目并不容易,方法之一时以参数_SC_OPEN_MAXd调用sysconf函数,然后动态分配一个合适大小的pollfd数组,然而sysconf也可能返回indeterminate(不确定),意味着我们仍然不得不猜测一个值,所以干脆直接用POSIX的OPEN_MAX常值了。





0 0