IO多路复用——select

IO多路复用编程

     是同步IO的一种,用一个进程一次等待多个IO就绪事件的发生,加大几率,尽量高效的等。数组

     适用场景服务器

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

  (2)当一个客户同时处理多个套接口时,而这种状况是可能的,但不多出现。多线程

  (3)若是一个TCP服务器既要处理监听套接口,又要处理已链接套接口,通常也要用到I/O复用。并发

  (4)若是一个服务器即要处理TCP,又要处理UDP,通常要使用I/O复用。socket

  (5)若是一个服务器要处理多个服务或多个协议,通常要使用I/O复用。ide

  与多进程和多线程技术相比,I/O多路复用技术的最大优点是系统开销小,系统没必要建立进程/线程,也没必要维护这些进程/线程,从而大大减少了系统的开销。函数


select函数测试

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型以下:

#include <sys/.h>
<sys/>
 ( maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,返回值:就绪描述符的数目,超时返回0,出错返回-1

函数参数介绍以下:

(1)第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(所以把该参数命名为maxfdp1),描述字0、一、2...maxfdp1-1均将被测试。由于文件描述符是从0开始的。

(2)中间的三个参数readset、writeset和exceptset指定咱们要让内核测试读、写和异常条件的描述字。若是对某一个的条件不感兴趣,就能够把它设为空指针。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);   // 检查集合中指定的文件描述符是否能够读写 

fd_set结构体是文件描述符集,该结构体其实是一个整型数组,数组中的每一个元素的每一位标记一个文件描述符。fd_set能容纳的文件描述符 数量由FD_SETSIZE指定,通常状况下,FD_SETSIZE等  于1024,这就限制了select能同时处理的文件描述符的总量。

(3)timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

这个参数有三种可能:

    1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

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

    3)根本不等待:检查描述字后当即返回,这称为轮询。为此,该参数必须指向一个timeval结构,并且其中的定时器值必须为0。

(4)返回值状况: 
a)超时时间内,若是文件描述符就绪,select返回就绪的文件描述符总数(包括可读、可写和异常),若是没有文件描述符就绪,select返回0; 
b)select调用失败时,返回 -1并设置errno,若是收到信号,select返回 -1并设置errno为EINTR。

(5)文件描述符的就绪条件: 
在网络编程中, 
1)下列状况下socket可读: 
a) socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT; 
b) socket通讯的对方关闭链接,此时该socket可读,可是一旦读该socket,会当即返回0(能够用这个方法判断client端是否断开链接); 
c) 监听socket上有新的链接请求; 
d) socket上有未处理的错误。 
2)下列状况下socket可写: 
a) socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT; 
b) socket的读端关闭,此时该socket可写,一旦对该socket进行操做,该进程会收到SIGPIPE信号; 
c) socket使用connect链接成功以后; 
d) socket上有未处理的错误。


selelct原理图

wKiom1ee582T0F4_AAGgKoA3mDk565.png

说明:

   一、select只负责等待IO,不负责对IO进行操做,由recv/send等函数进行

   二、select一共有两次系统调用:1)select系统调用 2)recvfrom系统调用

select原理概述

调用select时,会发生如下事情:

  1. 从用户空间拷贝fd_set到内核空间;

  2. 注册回调函数__pollwait;

  3. 遍历全部fd,对所有指定设备作一次poll(这里的poll是一个文件操做,它有两个参数,一个是文件fd自己,一个是当设备还没有就绪时调用的回调函数__pollwait,这个函数把设备本身特有的等待队列传给内核,让内核把当前的进程挂载到其中);

  4. 当设备就绪时,设备就会唤醒在本身特有等待队列中的【全部】节点,因而当前进程就获取到了完成的信号。poll文件操做返回的是一组标准的掩码,其中的各个位指示当前的不一样的就绪状态(全0为没有任何事件触发),根据mask可对fd_set赋值;

  5. 若是全部设备返回的掩码都没有显示任何的事件触发,就去掉回调函数的函数指针,进入有限时的睡眠状态,再恢复和不断作poll,再做有限时的睡眠,直到其中一个设备有事件触发为止。

  6. 只要有事件触发,系统调用返回,将fd_set从内核空间拷贝到用户空间,回到用户态,用户就能够对相关的fd做进一步的读或者写操做了。

select优势

  select模型是Windows sockets中最多见的IO模型。它利用select函数实现IO 管理。经过对select函数的调用,应用程序能够判断套接字是否存在数据、可否向该套接字写入据。

    如:在调用recv函数以前,先调用select函数,若是系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就能够调用recv函数接   收数据了。

   能够看出使用select模型,须要两次调用函数。第一次调用select函数第二次socket API。使用该模式的好处是:能够等待多个套接字。

select缺点

  1. 最大并发数限制:使用32个整数的32位,即32*32=1024来标识fd;

  2. 效率低:每次都会线性扫描整个fd_set,集合越大速度越慢;

  3. 内核/用户空间内存拷贝问题。



select实现TCP服务器wKiom1ee54uSuX-fAAKAHYfIoGU521.png

代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int fds[64];
const fds_nums=sizeof(fds)/sizeof(fds[0]);
static int startup(const char *ip,int port)
{
  int sock=socket(AF_INET,SOCK_STREAM,0);
  if(sock<0)
  {
    perror("socket");
    exit(2);
  }
  int opt = 1;
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  struct sockaddr_in local;
  local.sin_family=AF_INET;
  local.sin_port=htons(port);
  local.sin_addr.s_addr=inet_addr(ip);
  if(bind(sock,(struct sockaddr *)&local,sizeof(local))<0)
  {
    perror("bind");
    exit(3);
  }
  if(listen(sock,5)<0)
  {
    perror("listen");
    exit(4);
  }
  return sock;
}
static void usage(const char *proc)
{
  printf("%s [ip] [port]\n",proc);
}
int main(int argc,char *argv[])
{
  if(argc!=3)
  {
    usage(argv[0]);
    exit(1);
  }
  int listen_sock=startup(argv[1],atoi(argv[2]));
  fd_set rset;
  int i=0;
  FD_ZERO(&rset);
  FD_SET(listen_sock,&rset);
  //initial fds
  for(;i<fds_nums;i++)
  {
      fds[i]= -1;
  }
  fds[0]=listen_sock;
  int done=0;
  while(!done)
  {
    //reset current rset
    int max_fd= -1;
    for(i=0;i<fds_nums;i++)
    {
      if(fds[i]>0)
      {
        FD_SET(fds[i],&rset);
        max_fd=max_fd<fds[i]?fds[i]:max_fd;
      }
    }
    //struct timeval _ti={5,0};
    switch(select(max_fd+1,&rset,NULL,NULL,NULL))
    {
      case 0:
        printf("time out...\n");
        break;
      case -1:
        perror("select");
        break;
      default:
          for(i=0;i<fds_nums;i++)
          {
            //listen_fd
            if(i==0&&FD_ISSET(listen_sock,&rset))
            {
                //printf("there\n");
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                int newfd=accept(listen_sock,(struct sockaddr *)&peer,&len);
                if(newfd>0)
                {
                  printf("get a new client$ socket->%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
                }
                int j=0;
                for(j;j<fds_nums;j++)
                {
                  if(fds[j]== -1)
                  {
                    fds[j]=newfd;
                    break;
                  }
                }
                //mfull of queue
                if(j==fds_nums)
                {
                  close(newfd);
                }
            }  
            else//normal accept_fd
            {
             // printf("there\n");
              if(FD_ISSET(fds[i],&rset))
              {
               
                char buf[1024];
                memset(buf,'\0',sizeof(buf));
                ssize_t _s=read(fds[i],buf,sizeof(buf)-1);
                if(_s>0)
                {
                  buf[_s-1]='\0';
                  printf("client$ %s\n",buf);
                }
                else if(_s==0)
                {
                  printf("%d is read done..\n",fds[i]);
                  close(fds[i]);
                  fds[i]= -1;
                }
                else{
                  perror("read");
                }
                
              }
            }
          }
        break;
    }
  }
  return 0;
}

结果:wKiom1ee62mBjD5GAAEn4YXFzms772.png