多路复用IO----select

    上次我们介绍了五种基本的IO模型,在这五种里边多路转接IO是我们要了解的重点。

  

为什么他重要,我们先拿他和我们基础的IO来进行一下区分,当我们遇到普通的IO时都会开启一个新的进程来处理这个IO,在现在网络的访问量来开,如果同时有五千个IO来恐怕普通的机器都难以处理,更别说动不动就上亿的点击了,那这时候你的CPU占有率会相当的高,并且他并不是在干活如果没有数据的话就会阻塞在那里,浪费你的系统资源,所以就有了我们今天要介绍的多路复用IO,他是一个线程或者进程,通过他来统一管理我们的IO,这样就可以大大的提高服务器的吞吐能力。

就如上图,现在有1000个IO同时来访问机器,只有一个select进程来被系统调用,然后select会进行处理,当这1000个里边有哪个IO准备好了,可以操作了,他会通知对应的进程,这样哪个进程直接来处理就可以了。

我们来了多个IO通过多路复用IO模型来把他们管理起来,然后由这个IO来去通知对应进程是否可以操作。

多路复用主要有三种模型:select、poll、epoll这里我们分析一下他们各自的优缺点。

select模型

在Linux下我们可以通过man命令来查看select函数的详解

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

第一个参数:nfds is the highest-numbered file descriptor in any of the three sets, plus 1.意思是说这个参数应该是你要监听的文件描述符的个数+1,也就是文件描述符从0.....nfds-1都将会被监听处理。

对于有后边连续的三个参数

  Three  independent  sets  of  file  descriptors are watched.  Those listed in readfds will be watched to see if characters become available for reading (more precisely, to see if a read will not block; in particular, a file  descriptor  is  also ready  on end-of-file), those in writefds will be watched to see if a write will not block, and those in exceptfds will be watched for exceptions.  On exit, the sets are modified in place to indicate which file descriptors actually changed  status.   Each  of  the  three file descriptor sets may be specified as NULL if no file descriptors are to be watched for the corresponding class of events.

是说这三个集合是用来指定我们让内核来测试读、写、异常被去除的文件描述符,如果不感兴趣可以设置为NULL,struct fd_set是用来存放文件描述符的集合,可以通过 以下四个函数对他来进行设置。

void FD_ZERO(fd_set *fdset);           //清空集合

void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中

void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除

int  FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 

第五个参数timeout等待指定描述就绪的时间长度,这个参数有三种情况

当这个参数为空的时候会一直等待下去直到有一个IO准备好返回的时候

当这个参数结构体内有数据的时候在指定的时间内有描述符准备好的时候返回

当这个参数结构体内的数据时间为0的时候根本不等待,检查描述符后立即返回也就是我们说的轮询

//这是一个通过多路复用IO--select来实现的tcp服务端程序

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>

int main(int argc,char *argv[])
{
    if(argc!=3)
    {
        printf("port ip\n");
        return -1;
    }
     int sockfd=-1;
     char buff[1024]={0};
     int i=0;
     int j=0;
    struct sockaddr_in srv_addr;
    struct sockaddr_in cli_addr;
    int fd_arr[1024]={-1};//用于暂时存储文件描述符
    int max_fd=0;
    fd_set rds;
     sockfd=socket(AF_INET,SOCK_STREAM,0);
     if(sockfd<0)
	{
      perror("socket error\n");
	  return -1;
	}
    srv_addr.sin_family=AF_INET;
    srv_addr.sin_port=htons(atoi(argv[2]));
    srv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    socklen_t len=sizeof(struct sockaddr_in);
    int ret=bind(sockfd,(struct sockaddr*)&srv_addr,len);
    if(ret<0)
    {
        perror("bind error\n");
        return -1;
    }
    if(listen(sockfd,5)<0)
    {
        perror("listen error\n");
        return -1;
    }//之前和正常服务端一样
   for(i=0;i<1024;i++)
   {
       fd_arr[i]=-1;
   }
   fd_arr[0]=sockfd;//把监听描述符特殊处理先放进队列
    while(1)
    {
        max_fd=sockfd;
        FD_ZERO(&rds);//初始化结构体
        for( i=0;i<1024;i++)
        {
            if(fd_arr[i]!=-1)//如果数组中对应位置不为-1说明有文件描述符
            {
                FD_SET(fd_arr[i],&rds);//把文件描述符放入结构体
                max_fd=max_fd>fd_arr[i]?max_fd:fd_arr[i];//更新最大描述符的值
            }
        }
       struct timeval tv;
       tv.tv_sec=3;
       tv.tv_usec=0;
       ret=select(max_fd+1,&rds,NULL,NULL,&tv);//这里我的文件描述符是读事件的集合所以后边两个位NULL
       if(ret<0)//当返回值小于0代表失败
       {
           perror("select error\n");
           continue;
       }
       else if(ret==0)//当返回值等于0代表超时了没有数据来
       {
           printf("no data arrived\n");
           continue;
       }
       //返回值大于0代表正常这时候留在集合里边的都是已经就绪可以处理的文件描述符,返回的值就是文件描述符的个数
       for( i=0;i<max_fd+1;i++)
       {//没办法确定是哪个就绪了,所以需要遍历
           if(FD_ISSET(i,&rds))//检查对应描述符是够可以读写如果可以继续判断
           {
               if(i==sockfd)//判断是不是监听描述符,因为因为监听描述符和别的描述符操作不一样
               {//如果是监听描述符代表有新的链接来了。
                   int new_fd;
                   new_fd=accept(sockfd,(struct sockaddr*)&cli_addr,&len);
                   if(new_fd<0)
                   {
                       perror("accept error");
                       continue;
                   }
                   printf("have new connect \n");
                   for(j=0;j<1024;j++)
                   {
                       if(fd_arr[j]==-1)
                       {//把新连接的文件描述符放到暂存数据里边
                           fd_arr[j]=new_fd;
                           max_fd=max_fd>new_fd?max_fd:new_fd;
                           break;
                       }
                   }
               }
               else
               {//如果不是监听描述符就接收数据
                   memset(buff,0x00,1024);
                   ret=recv(i,buff,1023,0);
                   if(ret<=0)
                   {
                       if(errno==EAGAIN||errno==EINTR)
                       {//如果是read函数报出以上两个错误就再去尝试。
                           continue;
                       }
                       perror("recv error");
                       close(i);//不是以上两个错误代表读完了就关闭掉对应描述符
                       for(j=0;j<1024;j++)
                       {//关掉描述符之后并把对应的数组中的描述符删掉
                           if(i==fd_arr[j])
                           {
                               fd_arr[j]=-1;
                           }
                       }
                   }
                   printf("client say:%s\n",buff);

               }
       }
    }
    close(sockfd);
    return 0;
}
}

Selct有一个最大的优点:几乎所有平台都支持select,有着良好的跨平台性。

Select本质上是通过设置或者检查fd标志位的数据结构来进行下一步处理,这样就会存在一些缺点:

select最大缺陷就是单个进程打开的fd是有一定限制的,一般来说默认是1024。

Select在进行扫描的时候用的是线性扫描,也就是轮询的方式,效率极低。当套接字比较多的时候效率就很低。

每次调用select的时候都需要把fd集合从用户态拷贝到内核态,这样会消耗大量资源。

 

  •  

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页