第六章 I/O复用: select和poll函数

来源:百度文库 编辑:神马文学网 时间:2024/03/29 22:02:30

第六章 I/O复用: select和poll函数 6.1 Introduction
   前面的例子了客户端要同时处理标准输入和TCP socket  当输入阻塞时不能立即处理socket  所以需要采用I/O复用技术
   以下几个方面需要用到I/O复用技术:
   1.一个客户同时处理多个描述符(如前所述)
   2.客户同时处理多个socket
   3.TCP服务器同时处理监听端口和连接端口
   4.服务器同时处理TCP和UDP
   5.服务器同时处理多个服务或多个协议

6.2 I/O Models
   以UDP为例说明

 阻塞式I/O模型
   

   recvfrom()直到收到数据并复制给进程缓冲后返回  或者返程错误

 非阻塞式I/O模型
   

   当I/O操作请求不能立即完成时  不使进程进入休眠  而是立即返回错误信息
   前4个recvfrom()因数据未到而直接返回了EWOULDBLOCK
   应用程序在一个循环中反复这样调用  称为polling  这很占CPU时间

 I/O复用模型
   

   需要用到select()或poll()
   如select()会在数据准备好之后返回而后调用recvfrom()
   这个过程看似与阻塞式区别不大  但是在有多个描述符存在的情况下是很有利的!

 信号驱动I/O模型
   

   sigaction()建立一个信号处理函数  当即得到返回  直到数据到达后内核发送SIGIO信号给进程  

随后recvfrom()

 异步I/O模型
   

   由POSIX定义  但一般系统都未将之实现  其与信号驱动I/O相比是在数据复制完成后再返回调用
   前4个模型都可以成为是同步I/O

6.3 select Function
   高知内核等待某一或某些事件发生  而后唤醒进程  或超时返回
   int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *excepset,
              const struct timeval *timeout);    返回准备好的描述符数量 超时返0错误-1

   struct timeval{
     long tv_sec;    秒
     long tv_usec;   毫秒
   };
   1.该参数用以指明等待的时间:
     a.永远等待  设置NULL指针
     b.不等待立即返回  2个元素都设为0
     c.等待一定时间  具体设置之
   2.并不十分精确  最大精度10ms  但强于sleep()的1s
   3.因为有const修饰符  所以该值不会被修改  若要计算时间2次调用时间函数求差时

   readset writeset excepset用以标示我们需要内核进行检测读写错误的描述符
   1.excepset只有两种情况  一个是out-of-band  另一个书上没详细说
   2.用以组整数数组来标识  每一位对应一个描述符  如果array[0]指示0~31个描述符  常用4个宏进行操作
     void FD_ZERO(fd_set *fdset);           清除所有位
     void FD_SET(int fd, fd_set *fdset);    设置位
     void FD_CLR(int fd, fd_set *fdset);    关闭位
     void FD_ISSET(int fd, fd_set *fdset);  查询该位是否设置
   3.任何一个XXXset设为NULL表示对该状态不关心  全NULL可以代替sleep()使用
   4.value-result参数  送时表明哪些描述符需要检测  返回时表明哪些描述符准备完毕

   maxfdp1意思是最大描述符号加1
   FD_SETSIZE定义fd_set描述符数量  但通常过大所以用这个参数减小系统负担

 何时描述符准备完毕
   书上列举很详细  比较容易想到  不记了
6.4 str_cli Function (Revisited)
   void str_cli(FILE *fp, int sockfd)
   {
     int    maxfdp1;
     fd_set rset;
     char   sendline[MAXLINE], recvline[MAXLINE];

     FD_ZERO(&rset);      先要清空所有描述符
     for(;;) {
       FD_SET(fileno(fp), &rset);    设置描述符  fileno()将标准I/O指针转为对应描述符
       FD_SET(sockfd, &rset);
       maxfdp1=max(fileno(fp), sockfd)) + 1;
       Select(maxfdp1, &rset, NULL, NULL, NULL);

       if(FD_ISSET(sockfd, &rset) {        socket可读
         if(Readline(sockfd, recvline, MAXLINE) ==0)
           err_quit(...);
         Fputs(recvline, stdout);
       }

       if(FD_ISSET(fileno(fp), &rset) {    标准I/O可读
         if(Fgets(sendline, MAXLINE, fp) == NULL)
           return;
         Writen(sockfd, sendline, strlen(sendline);
       }
     }
   }

   该改进程序处理如下情况:
   1.对方TCP发数据  本方socket可读  read()返回大于0值
   2.对方TCP发FIN  本方socket可读  read()返回0即EOF
   3.对方TCP发RST  本方socket可读  read()返回-1并由errno指明错误

6.5 Batch Input and Buffering
  

   时刻7和8时候在网络上的报文流向
   在前一节改进的str_cli程序中  当输入数据都完成后系统得到EOF并一路返回到main()  而这时并没有完成程序的所有功能  没有接受到返回消息
   所以在大批输入和缓冲的情况下  仍要改进程序  方法是半关闭TCP连接shutdown()
   发送FIN告知对方  本方已没有数据要发送  但socket仍开放等待读取

6.6 shutdown Function
   函数功能上一节已经描述了
   


   int shutdown(int sockfd, int howto);    OK返0错误-1
   howto有三个选择(括号里表示定义的数值):
   SHUT_RD(0)  :  连接的读功能关闭-socket不会收到任何数据  当前接受缓冲数据亦被丢弃  读函数无效  再接受到的数据会被ACK然后直接丢弃
   SHUT_WR(1)  :  连接的写功能关闭-当前发送缓冲数据会被发送  然后进入半关闭流程  写函数无效  
   SHUT_RDWR(2):  如同RD WR同时使用

6.7 str_cli Function (Revisited Again)
   void str_cli(FILE *fp, int sockfd)
   {
**    int    maxfdp1, stdineof;
     fd_set rset;
**    char buf[MAXLINE];
**    int    n;

     stdioof = 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) {
         if(
           if(stdioeof == 1)     仅当输入EOF关闭后才可能退出程序
             return;
           else
             err_quit(...);
         }
         Writen(fileno(stdout), buf, n);
       }

       if(FD_ISSET(fileno(fp), &rset) {    
         if((n=Read(fileno(fp), buf, MAXLINE) ==0) {
           stdioeof=1;
           Shutdown(sockfd, SHUT_WR);    半关闭
           FD_CLR(fileno(fp), &ret);     输入设备关闭描述符
           continue;
         }
         Writen(sockfd, buf, n);
       }
    }

   stdineof的作用  仅当输入确认完成后置1  随后才可能退出socket
   注意程序中只用到read() write()  因为select()可以代替readn writen的作用

6.8 TCP Echo Server (Revisited)
   第五章中的服务器程序对每一个客户开启一个进程  这一节的程序仅使用一个进程  用select()处理不同的客户

   举一个实例:
   Figure 6.15
   1.服务器仅有一个监听socket时候的TCP数据结构
     当服务器在前台运行  rset中描述符0 1 2分别针对标准输入 输出 错误设备  所以第一个可分配给监听socket的描述符号是3
     另外还有一个数值型client[]  标明分配给每个客户的连接socket描述符  初始化为-1
   

   2.新连接accept()后返回描述符4  后一个连接返回5
     rset相应位置置1  client[]置描述符号
   

   3.第一个连接关闭  rset相应位置置0  client[]置-1
     注意!  maxfd值没有改变

   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];
     pid_t     childpid;
     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;              记录最大的描述名号
     maxi =-1;                    记录client[]用到的最大数目
     for(i=0; i< FD_SETSIZE; i++)
       client
=-1;              初始-1
     FD_ZERO(&allset);
     FD_SET(listenfd, &allset);   仅关注listenfd操作

     for(;;) {
**      rset=allset;
       nready=Select(maxfd+1, &rset, NULL, NULL, NULL);

       if(FD_ISSET(listenfd, &rset) {    有新连接接收
         clilen=sizeof(cliaddr);
         connfd=Accept(listenfd, (SA*)&cliaddr, &clilen);

         for(i=0; i           if(client
<0) {
             client
=connfd;
             break;
           }
         if(i==FD_SETSIZE)               连接过多退出
           err_quit(...)

         FD_SET(connfd, &allset);        新增关注的描述符
         if(connfd>maxfd)                是否增加一个新单元
           maxfd=connfd;
         if(i>maxi)                      
           maxi=i;

         if(--nready<=0)                 当没有可读描述符了
           continue;
       }

       for(i=0; i<=maxi; i++) {          检查所以客户的数据请求
         if((sockfd=client
< 0)
           continue;
         if(FD_ISSET(sockfd, &rset) {
           if((n=Read(sockfd,buf, MAXLINE)) == 0) {
             Close(sockfd);
             FD_CLR(sockfd, &allset);
             client
=-1;
           } else
             Writen(sockfd, buf, n);

           if(--nready<=0)               当没有可读描述符了
             break;
         }
       }
     }
   }

   这个版本还存在的问题是DOS拒绝服务
   某客户发送一个字节后进入休眠  服务器在下次进入Read()会被阻塞     (不是很明白!!!)
   所以结论是  在处理多客户时  服务器绝不能调用只于一个客户相关的函数  否则被被阻塞而不能提供其他客户服务
6.9 pselect Function
   以后会有例子  见到再说

6.10 poll Funtion
   poll()功能同select()  但能更好处理流设备
   int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);  返回值同select()
   1.struct pollfd {
       int   fd;        需要检查的描述符名
       short events;    检测的情况
       short revents;   检测后返回的情况
     }
     Figure 6.23
   2.poll()定义了三个类别:  普通 优先带 高优先  这些词汇来自于基于流的系统实现  以后有更详细解释
   3.nfds记录了pollfd{}里的元素数
   4.timeout的设置
     INFTIM  永远等待
     0       立即返回
     >0      一段时间  以ms为单位(没这么精确而以)
   5.不需要处理某个描述符的时候  只要将fd置负数events就会被忽略而revents会被填0

6.11 TCP Echo Server (Revisited Again)
   poll()的一个实例  不需要waitpid()了

   int main(int argc, char **argv)
   {
     int       i, maxi, listenfd, connfd, sockfd;
     int       nready;
     ssize_t   n;
     char      buf[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;            第一个元素赋给listenfd
     client[0].events=POLLRDNORM;
     for(i=1; i       client[i].fd=-1;
     maxi=0;

     for(;;) {
       nready=Poll(client, maxi+1, INFTIM);

       if(client[0].revents & POLLRDNORM) {    新连接
         clilen=sizeof(cliaddr);
         connfd=Accept(listenfd, (SA*)&cliaddr, &clilen);

         for(i=1; i           if(client[i].fd < 0) {
             client[i].fd=connfd;
             break;
           }
         if(i == OPEN_MAX)             连接过多
           err_quit(...)

         client[i].events=POLLRDNORM;  等待poll()检测
         if(i>maxi)                      
            maxi=i;

         if(--nready<=0)
             continue;
       }

       for(i=1; i<=maxi; i++) {        检测所有客户
         if((sockfd=client[i].fd) < 0)
           continue;
         if(client[i].revents & (POLLRDNORM | POLLERR) {
           if((n=read(sockfd, buf, MAXLINE)) < 0) {
             if(errno==ECONNRESET) {   连接RST
               Close(sockfd);
               client[i].fd=-1;
             }else
               err_sys(...)            读错误
           }else if(n==0) {            客户关闭连接
             Close(sockfd);
             client[i].fd=-1;
           }else
             Writen(sockfd, buf, n);

           if(--nready <= 0)
             break;
         }
       }
     }
   }