IO多路复用之select

5. io多路复用
5.1 如何解决同时“读鼠标”和“读键盘”的问题
  1)多进程实现
  2)多线程
  3)将“读鼠标”和“读键盘”设置为非阻塞实现
  4)多路IO
5.2 有关多路IO
  (1)多路IO的工作原理
    使用多路IO时,不需要多进程、多线程以“多线任务”方式实现,也不需要用到非阻塞
    以阻塞读为例 select1.png

    27.IO多路复用之select_linux网络编程

    

如果是阻塞写的话,需要将文件描述符加入写集合,不过我们说过对于99%的情况来说,写操作不会阻塞,所以一般情况下对于写来说,使用多路Io没有意义。

注意:对于多路io来说,只有操作阻塞的fd才有意义,如果文件描述符不是阻塞的,使用多路IO没有意义
(2)多路IO有什么优势
  比如以同时读写鼠标、读键盘为例,如果使用,
  1)多进程实现
    开销太大,绝对不建议这么做。
  2)非阻塞方式
    cpu空转,耗费cpu资源,不建议
  3)多线程
  常用方法,不过多路IO也是一个不错的方法。

  4)多路IO
  使用多路IO时,多路IO机制由于在监听时如果没有动静的话,监听会休眠,因此开销也很低,相比多进程和非阻塞来说,多路IO机制也是很不错的方式
(3)select和poll
  多路IO有两种实现方式,分别是poll和select,其中select会比poll更常用些

5.2 多路io之select机制

5.2.1 select函数
(1)函数原型
  #include <sys/select.h>
  int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

1)功能
  监听集合中的描述符有没有动静,如果没有动静就阻塞。
  如果有动静就成功返回,返回值为集合中有动静的fd的数量
  什么是有动静?
  比如以读为例,如果fd有数据来了需要被read时,这就是动静
2)参数
(a)nfds:
  nfds = readfds, writefds, exceptfds这三个集合中值最大的那个描述符+1。
  nfds用于说明需要关心描述符的范围,这个范围必须覆盖所有集合中的文件描述符。
  比如“读集合”中包含0,3,6这三个文件描述符,写集合和异常集合为NULL(没有)nfds==6+1
  表示监听范围包含7描述符,描述符因为是从0算起的,所以监听范围所包含的描述符为0、 1、 2、 3、 4、 5、 6

疑问:集合中只包含了0、3、6三个,但是为什么需要监听的有这么多,只能说人家select
机制就是这么实现的,这个没办法
(b)readfds、writefds、exceptfds:读、写、异常集合。
  · readfds:读结合,放读会阻塞的文件描述符
  · writefds:写集合,放写会阻塞的描述符。
  · exceptfds:放会异常出错的文件描述符。

常用的是读集合,写集合和异常集合基本用不到,所以这里不做介绍,写、异常集合不用时就写NULL。

至于如何将文件描述符放入集合中,我们使用如下带参宏来实现。
  void FD_CLR(int fd, fd_set *set):将fd从集合set中清除,这个宏不常用
  int FD_ISSET(int fd, fd_set *set):判断是不是set中的fd有动静了
  void FD_SET(int fd, fd_set *set):将fd放到集合set中
  void FD_ZERO(fd_set *set):将整个集合全部清空
比如:
  fd_set readfds;//定义一个读集合
  FD_ZERO(&readfds); //把集合全部清空
  FD_SET(fd, &readfs);//将fd放到readfds集合

(d)timeout:用于设置阻塞超的时间
  select函数监听集合时,如果没有任何动静的话就阻塞(休眠)。
  如果timeout被设置为NULL的话,select会永远阻塞下去,直到被信号中断或者集合中的某些文件描述符有动静了

  如果你不想休眠太久的话,就可以设置超时时间,如果时间到了但是集合中的fd还没有任何动静,
  select就返回,然后不再阻塞,超时时的返回的值为0。

  成员结构如下:
  

struct timeval 
{
long tv_sec; /* seconds(秒) */
long tv_usec; /* microseconds (微秒)*/
};

  · tv_sec:设置秒
  · tv_usec:设置微妙
  时间精度为微妙,也就是说可以设置一个精度为微妙级别的超时时间
  由于select函数有超时功能,实际上可以使用select模拟出一个微妙级精度的定时器

3)返回值
(a)-1:说明函数调用失败,errno被设置。
  select调用出错的情况有很多种,比如select在阻塞时被信号唤醒从而导致出错返回,
  errno被设置为EINTR错误号,这个错误号表示函数是被信号中断而出错返回的

  如果不想被信号中断,
    · 我们可以自己忽略、屏蔽这些信号
    · 手动重启select的调用

  lable: ret = select(...);
  if(ret==-1 && errno==EINTR) goto lable;
  else if(ret == -1) print_err(...);
(b)0:超时时间到并且集合中没有一个描述符有响应时,就返回0。
(c)>0:集合中fd有动静时,函数返回有动静的文件描述符的数量

(2)代码演示
  select每次重新监听时需要重新设置“集合”和“超时时间”,因为每次select监听结束时会清空“集合”和“超时时间”

  select.c

  

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <sys/select.h>
#include <sys/time.h>  
#include <unistd.h>

void print_err(char *str,int line,int err_no){
    printf("%d,%s:%s\n",line,str,strerror(err_no) );
    exit(-1);
}

int main(int argc, char const *argv[])
{
    int coord=0;
    int mousefd =-1;
    fd_set readfds;

    char buf[100]={0};
    int ret= 0;

    struct timeval timeover;
    

    mousefd =open("/dev/input/mouse1",O_RDONLY); //可能Mouse0或2
    if(mousefd == -1) print_err("open mouse0 fail",__LINE__,errno);
    

    while(1){
        /*将0,mousefd加入读集合*/
        FD_ZERO(&readfds); //把集合全部清空
        FD_SET(0, &readfds);//将fd放到readfds集合中
        FD_SET(mousefd, &readfds);
        // 设置超时时间
        timeover.tv_sec=3;
        timeover.tv_usec=0;
        // 如果集合没动静就阻塞
        label:ret=select(mousefd+1,&readfds,NULL,NULL,&timeover);
        if(ret == -1 && errno == EINTR) goto label;
        else if(ret ==-1) print_err("select fail",__LINE__,errno);
        else if(ret >0){
            if(FD_ISSET(0, &readfds)){
                bzero(buf,sizeof(buf));
                ret =read(0,buf,sizeof(buf));
                if(ret>0) printf("%s\n",buf );
            }
            if(FD_ISSET(mousefd, &readfds)){
                bzero(&coord,sizeof(coord));
                ret =read(mousefd,&coord,sizeof(coord));
                if(ret>0) printf("%u\n",coord);
            }
        }else if(ret ==0){
            printf("timeover\n");
        }

    }
    return 0;
}