select

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

    多端口复用函数select在调用前要首先设置监听的端口数目,FD_ZERO是清空端口集,FD_SET是设置端口集。

    select()函数常常用在用一个进程监听多个服务器端socket

    有时,select()也被当作延时函数使用。sleep()延时会释放CPU,select()的话,可以在占用CPU的情况下延时。

    select()函数主要是建立在fd_set类型的基础上的。fd_set(它比较重要所以先介绍一下)是一组文件描述字(fd)的集合,它用一位来表示一个fd(下面会仔细介绍),对于fd_set类型通过下面四个来操作:

    fd_set  set;

    FD_ZERO(&set);        /*将set清零使集合中不含任何fd*/

    FD_SET(fd, &set);      /*将fd加入set集合*/

    FD_CLR(fd, &set);      /*将fd从set集合中清除*/

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

    过去,一个fd_set通常只能包含<32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd; 现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,这样就能表示<1024的fd。根据fd_set的位矢量实现,我们可以重新理解操作fd_set的四个宏:

    fd_set  set;

    FD_ZERO(&set);        /*将set的所有位置0,如set在内存中占8位则将set置为00000000*/

    FD_SET(0, &set);       /*将set的第0位置1,如set原来是00000000,则现在变为100000000,这样fd==1的文件描述字就被加进set中了*/

    FD_CLR(4, &set);       /*将set的第4位置0,如set原来是10001000,则现在变为10000000,这样fd==4的文件描述字就被从set中清除了*/

    FD_ISSET(5, &set);     /*测试set的第5位是否为1,如果原来set是10000100,则返回非零,表明fd==5的文件描述符在set中,否则返回0*/

    注意:fd的最大值必须<FD_SETSIZE。

    select函数的接口比较简单:

    int select(int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout);

    功能:

    测试指定的fd可读?可写?有异常条件待处理?

    参数:

    nfds: 需要检查的文件描述字个数(即检查到fd_set的第几位),数值应该比三组fd_set中所含的最大fd值更大,一般设为三组fd_set中所含的最大fd值加1(如在readset, writeset, exceptset中所含最大的fd为5,则nfds=6,因为fd是从0开始的 )。设这个值是为了提高效率,使函数不必检查fd_set的所有1024位。

    readset: 用来检查可读性的一组文件描述字。

    writeset: 用来检查可写性的一组文件描述字。

    exceptset: 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)

    timeout: 有三种可能:

    1.  timeout = NULL (阻塞:直到有一个fd位被置为1函数才返回)

    2.  timeout所指向的结构设为非零时间(等待固定时间:有一个fd位被置为1或者时间耗尽,函数均返回)

    3.  timeout所指向的结构,时间设为0(非阻塞:函数检查完每一个fd后立即返回)

    返回值:返回对应位仍然为1的fd的总数。

    Remark:

    三组fd_set均将某些fd位 置0,只有那些可读,可写以及有异常条件待处理的fd位仍然为1。

    使用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

        TIMEVAL tv;

    #else

        struct timeval   tv;

    #endif    /*_WIN32*/

        FD_ZERO(&rfds);

        FD_ZERO(&wfds);

        if(rd)    //TRUE

          FD_SET(*s, &rfds);  //添加要测试的描述字

        if(wr)

          FD_SET(*s, &wfds);

        tv.tv_sec = timems/1000;  //seconds

        tv.tv_usec = timems%1000;  //ms

        for(;;)  //如果errno==EINTR,反复测试缓冲区的可读性

          switch(select((*s)+1, &fds, &wfds, NULL, (timems==TIME_INFINITE?NULL:&tv)))  //测试在规定的时间内套接字接口接收缓冲区是否有数据可读

          {

               // 0——超时,  -1——出错

               case 0:  /*time out*/

                   return 0;

               case (-1):    /*socket error*/

                  if( SocketError()==EINTR )

                     break;

                  return 0;  //有错但不是EINTR

               default:

                  if(FD_ISSET(*s, &rfds))  //如果s是fds中的一员返回非0,否则返回0

                     return 1;

                  if(FD_ISSET(*s, &wfds))

                     rerun  2;

                  return 0;

          };

    }






part 2

select原型: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

 

和select模型紧密结合的四个宏:

FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);


理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空

基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。对调整fd_set的大小可参考http://www.cppblog.com/CppExplore/archive/2008/03/21/45061.html中的模型2,可以有效突破select可监控的文件描述符上限。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有事件发生)。

 

另外,如果select调用中设置了等待时间,那么每次调用时都需要重新对这个时间赋值么?就像对fd_set处理一样。
例如:
fd_set readfd;
struct timval tv;
while(1) {
  FD_ZERO(&readfd);
  FD_SET(fd, &readfd);
  tv.tv_sec = 2;
  tv.tv_usec = 0;
  select(maxfd+1, &readfd, NULL, NULL, &tv);
  ......;
}

如上代码,对fd_set需要每次调用都要重新设置,那么对tv来说是否也是一样呢?能不能把对tv的赋值放在while外面?

答案是不行,如果将时间的初始化放在外边,时间初始化为2秒,假设在1秒后发上了事件,则select将会返回并将tv的时间变成上次阻塞的剩余时间,即1秒,然后再进行监视套接字。这是因为linux系统对select()的实现中会修改参数tv为剩余时间。所以对于select函数中的最后一个参数,需要在循环中设置,每次循环要重新设置。如果设在循环外面,当循环执行起来后,每次循环select都会修改tv的值,tv的值越来越小,导致最后会产生select函数这tv时间内收不到有效时间,而返回-1,造成错误。


另一点需要注意:如果在缓存区中如果存在仍然未读取完的数据,则再次循环执行select时会直接返回,不会阻塞,继续读取,。类似LT模式

你可能感兴趣的:(select)