select()函數以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET


select函數用於在非阻塞中,當一個套接字或一組套接字有信號時通知你,系統提供select函數來實現多路複用輸入/輸出模型,原型: 
         #include <sys/time.h> 
         #include <unistd.h> 
         int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout); 
     參數maxfd是需要監視的最大的文件描述符值+1;rdset,wrset,exset分別對應於需要檢測的可讀文件描述符的集合,可寫文件描述符的集 合及異常文件描述符的集合。struct timeval結構用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函數返回,返回值爲0。 
     fd_set(它比較重要所以先介紹一下)是一組文件描述字(fd)的集合,它用一位來表示一個fd(下面會仔細介紹),對於fd_set類型通過下面四個宏來操作: 
      FD_ZERO(fd_set *fdset);將指定的文件描述符集清空,在對文件描述符集合進行設置前,必須對其進行初始化,如果不清空,由於在系統分配內存空間後,通常並不作清空處理,所以結果是不可知的。 
      FD_SET(int fd, fd_set *fdset);用於在文件描述符集合中增加一個新的文件描述符。 
      FD_CLR(int fd, fd_set *fdset);用於在文件描述符集合中刪除一個文件描述符。 
      FD_ISSET(int fd, fd_set *fdset);用於測試指定的文件描述符是否在該集合中。         
     過去,一個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);      
     FD_SET(0, &set);    
     FD_CLR(4, &set);      
     FD_ISSET(5, &set);    
―――――――――――――――――――――――――――――――――――――――
注意fd的最大值必須<FD_SETSIZE。
―――――――――――――――――――――――――――――――――――――――

     select函數的接口比較簡單:
     int select(int nfds, fd_set *readset, fd_set *writeset,fd_set* exceptset, struct tim *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
     用於描述一段時間長度,如果在這個時間內,需要監視的描述符沒有事件發生則函數返回,返回值爲0。 
     有三種可能:
       1.timeout=NULL(阻塞:select將一直被阻塞,直到某個文件描述符上發生了事件)
       2.timeout所指向的結構設爲非零時間(等待固定時間:如果在指定的時間段裏有事件發生或者時間耗盡,函數均返回)
       3.timeout所指向的結構,時間設爲0(非阻塞:僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生)

返回值:      
     返回對應位仍然爲1的fd的總數。

Remarks:
     三組fd_set均將某些fd位置0,只有那些可讀,可寫以及有異常條件待處理的fd位仍然爲1。

舉個例子,比如recv(),    在沒有數據到來調用它的時候,你的線程將被阻塞,如果數據一直不來,你的線程就要阻塞很久.這樣顯然不好.    
所以採用select來查看套節字是否可讀(也就是是否有數據讀了)    
步驟如下——    
socket    s;    
.....    
fd_set    set;    
while(1)    
{        
       FD_ZERO(&set);//將你的套節字集合清空    
       FD_SET(s,    &set);//加入你感興趣的套節字到集合,這裏是一個讀數據的套節字s    
       select(0,&set,NULL,NULL,NULL);//檢查套節字是否可讀,    
                                                         //很多情況下就是是否有數據(注意,只是說很多情況)    
                                                         //這裏select是否出錯沒有寫    
       if(FD_ISSET(s,    &set)    //檢查s是否在這個集合裏面,    
       {                                    //select將更新這個集合,把其中不可讀的套節字去掉    
                                           //只保留符合條件的套節字在這個集合裏面                           
               recv(s,...);    
       }    
       //do    something    here    

     理解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(1)可以有效突破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模型的服務器模型:
array[slect_len];
nSock=0;
array[nSock++]=listen_fd;(之前listen port已綁定並listen)
maxfd=listen_fd;
while{
    FD_ZERO(&set);
    foreach (fd in array) 
    {
        fd大於maxfd,則maxfd=fd
        FD_SET(fd,&set)
    }
    res=select(maxfd+1,&set,0,0,0);
    if(FD_ISSET(listen_fd,&set))
    {
        newfd=accept(listen_fd);
        array[nsock++]=newfd;
             if(--res=0) continue
    }
    foreach 下標1開始 (fd in array) 
    {
        if(FD_ISSET(fd,&set))
           執行讀等相關操作
           如果錯誤或者關閉,則要刪除該fd,將array中相應位置和最後一個元素互換就好,nsock減一
              if(--res=0) continue
    }
}
     使用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 tim 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
uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems)     
{
      fd_set rfds,wfds;
#ifdef _WIN32
      TIM tv;
#else
      struct tim tv;
#endif     
      FD_ZERO(&rfds);
      FD_ZERO(&wfds); 
      if (rd)      //TRUE
      FD_SET(*s,&rfds);    //添加要測試的描述字 
      if (wr)      //FALSE
        FD_SET(*s,&wfds); 
      tv.tv_sec=timems/1000;      //second
      tv.tv_usec=timems%1000;      //ms 
      for (;;) //如果errno==EINTR,反覆測試緩衝區的可讀性
           switch(select((*s)+1,&rfds,&wfds,NULL,
               (timems==TIME_INFINITE?NULL:&tv))) //測試在規定的時間內套接口接收緩衝區中是否有數據可讀
          {                                               //0--超時,-1--出錯
          case 0:     
               return 0; 
          case (-1):    
               if (SocketError()==EINTR)
                    break;               
               return 0; //有錯但不是EINTR 
           default:
               if (FD_ISSET(*s,&rfds)) //如果s是fds中的一員返回非0,否則返回0
                    return 1;
               if (FD_ISSET(*s,&wfds))
                    return 2;
               return 0;
          };
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章