Linux Select

select系统调用时用来让我们的程序监视多个文件句柄的状态变化的。程序会停在select这里等待,直到被监视的文件句柄有一个或多个发生了状态改变。

1.函数原型

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

1.1 函数参数说明

(1)nfds: 所传入的最大文件描述符+1。所有加入集合的句柄值的最大那个那个值还要加1.比如我们创建了3个句柄。

int sa, sb, sc;
sa = socket(……);
connect (sa,….);
 
sb = socket(….);
connect (sb,…);

sc = socket(….);
connect(sc,…);

FD_SET(sa, &rdfds);
FD_SET(sb, &rdfds);
FD_SET(sc, &rdfds);

在使用select函数之前,一定要找到3个句柄中的最大值是哪个,我们一般定义一个变量来保存最大值,取得最大socket值如下。

int maxfd = 0;
if(sa > maxfd) maxfd = sa;
if(sb > maxfd) maxfd = sb;
if(sc > maxfd) maxfd = sc; 

然后调用select函数:

ret = select (maxfd+1, &rdfds, NULL, NULL,&tv);

(2) readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。其中,exceptset是特殊情况,即句柄上有特殊情况发生时系统会告诉select函数返回。特殊情况比如对方通过一个socket句柄发来了紧急数据,可用于处理带外数据。

(3) timeout: 用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间

1.2 返回值

返回值分为三种情况:

  1. 做好准备的文件描述符的个数
  2. 返回0:超时;
  3. 返回-1:错误;

2.相关数据结构与函数

2.1 struct timeval

struct timeval{      
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    }

timeval 的值有三种情况:
(1)timeout == NULL :等待无限长的时间。等待可以被一个信号中断。当有一个描述符做好准备或者是捕获到一个信号时函数会返回。如果捕获到一个信号, select函数将返回 -1,并将变量 erro设为 EINTR。

(2)timeout->tv_sec = 0 且timeout->tv_usec = 0:不等待,直接返回。加入描述符集的描述符都会被测试,并且返回满足要求的描述符的个数。这种方法通过轮询,无阻塞地获得了多个文件描述符状态。

(3)timeout->tv_sec !=0或timeout->tv_usec!= 0: 等待指定的时间。当有描述符符合条件或者超过超时时间的话,函数返回。在超时时间即将用完但又没有描述符合条件的话,返回 0。对于第一种情况,等待也会被信号所中断。

2.2 fd_set

https://blog.csdn.net/bailyzheng/article/details/7477446
数据结构fd_set实际上是一long类型的数组,每一个数组元素都能与一打开的文件句柄 (不管是socket句柄,还是其他文件或命名管道或设备句柄) 建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fe_set的内容,由此来通知执行了select()的进程哪一socket或文件可读。

fd_set的操作可通过如下宏完成:
(1)FD_ZERO(&set): /将set清零使集合中不含任何fd/

(2)FD_SET(fd, &set): /将fd加入set集合/
如监控标准输入:FD_SET( 0 ,&set)

句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。

(3)FD_CLR(fd, &set): /将fd从set集合中清除/

(4)FD_ISSET(fd, &set): /测试fd是否在set集合中/

3.编程模型

使用select函数的过程一般是:先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,相应位是否仍然为1。

以下是一个测试单个文件描述字可读性的例子:

 int  isready(int  fd)
    {
        int    rc;
        fd_set    fds;
        struct timeval    tv;
        FD_ZERO(&fds);
        FD_SET(fd,  &fds);
        tv.tv_sec = tv.tv_usec = 0;
    
        rc = select(fd+1, &fds, NULL, NULL, &tv);
        if( rc<0 )  //error
          return -1;
        return FD_ISSET(fd, &fds)  ? 1: 0;
    }

下面还有一个复杂一些的应用:

 //这段代码将指定测试Socket的描述字的可读可写性,因为Socket使用的也是fd
    unit32  SocketWait(TSocket* s,  bool rd,  bool wr,  unit32 timems)
    {
        fd_set  rfds, wfds;
       #ifdef _WIN32   //条件编译,根据OS类型,选择合适的数据类型
          TIMEVAL tv;
      #else
       struct timeval   tv;
      #endif    /*_WIN32*/
     
      //初始化重置fd_set数据结构
        FD_ZERO(&rfds); 
        FD_ZERO(&wfds);
   
       //设置要监听的fd文件描述符
        if(  rd )    //如果socket描述符正常(fd=0,则为标准输入,其他正常的文件描述符>0)
          FD_SET(*s, &rfds);  //添加要测试的描述字
        if( wr ) //如果socket描述符正常(fd=0,则为标准输入,其他正常的文件描述符>0)
          FD_SET(*s, &wfds);

        tv.tv_sec = timems/1000;  //seconds
        tv.tv_usec = timems%1000;  //ms
 
        //进入循环,不停的监控-处理fd情况
        for(;;)  //如果errno==EINTR,反复测试缓冲区的可读性
          switch(select((*s)+1, &fds, &wfds, NULL, (timems==TIME_INFINITE?NULL:&tv)))  //测试在规定的时间内套接字接口接收缓冲区是否有数据可读
          {          
               case 0:  /*time out*/
                    return 0;
               case (-1):    /*socket error*/
                  if( SocketError()==EINTR )
                     break;
                  return 0;  //有错但不是EINTR
               default://一般是要轮训检测所有的fds中的文件描述符,然后各个处理
                  if(FD_ISSET(*s, &rfds))  
                      handle***
                  if(FD_ISSET(*s, &wfds))
                      handle***
                  return 0;
          };
    }

4.关于select的阻塞

支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。

select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。

select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。

注意:
select进程被唤醒的过程。由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。

4 select总结

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat/proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

5. Unix5中IO模型

Unix下共有五种I/O模型
a. 阻塞I/O
b. 非阻塞I/O
c. I/O复用(select和poll)
d. 信号驱动I/O(SIGIO)
e. 异步I/O(Posix.1的aio_系列函数)

5.1阻塞I/O模型

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。
如果数据没有准备好,一直等待。。。。
数据准备好了,从内核拷贝到用户空间
I/O函数返回成功指示

在这里插入图片描述

5.2 非阻塞I/O模型

我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试 数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。
在这里插入图片描述

5.3 I/O复用模型

I/O复用模型会用到select或者poll函数,这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
在这里插入图片描述

5.4 信号驱动I/O模型

首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
在这里插入图片描述

5.5 异步I/O模型

调用aio_read函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。
在这里插入图片描述

5.6 几种I/O模型的比较

前四种模型的区别是第一阶段基本相同,第二阶段基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步I/O的两个阶段都不同于前四个模型。
在这里插入图片描述

5.7 同步I/O和异步I/O

a. 同步I/O操作引起请求进程阻塞,直到I/O操作完成。
异步I/O操作不引起请求进程阻塞。
b. 我们的前四个模型都是同步I/O,只有最后一个异步I/O模型是异步I/O。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值