Linux中對文件描述符的操作(FD_ZERO、FD_SET、FD_CLR、FD_ISSET)

轉載自:https://blog.csdn.net/cstarbl/article/details/7645298

在Linux中,內核利用文件描述符(File Descriptor)即文件句柄,來訪問文件。文件描述符是非負整數。打開現存文件或新建文件時,內核會返回一個文件描述符。讀寫文件也需要使用文件描述符來指定待讀寫的文件。宏FD_ZERO、FD_SET、FD_CLR、FD_ISSET中“FD”即爲file descriptor的縮寫,下面來一一進行介紹。

首先介紹一個重要的結構體:fd_set,它會作爲下面某些函數的參數而多次用到,fd_set可以理解爲一個集合,這個集合中存放的是文件描述符(file descriptor),即文件句柄,它用一位來表示一個fd(下面會仔細介紹)。fd_set集合可以通過下面的宏來進行人爲來操作。

1》FD_ZERO

用法:FD_ZERO(fd_set*);

用來清空fd_set集合,即讓fd_set集合不再包含任何文件句柄。在對文件描述符集合進行設置前,必須對其進行初始化,如果不清空,由於在系統分配內存空間後,通常並不作清空處理,所以結果是不可知的。

2》FD_SET

用法:FD_SET(int ,fd_set *);

用來將一個給定的文件描述符加入集合之中。

3》FD_CLR

用法:FD_CLR(int ,fd_set*);

用來將一個給定的文件描述符從集合中刪除。

4》FD_ISSET

用法:FD_ISSET(int ,fd_set*);

檢測fd在fdset集合中的狀態是否變化,當檢測到fd狀態發生變化時返回真,否則,返回假(也可以認爲集合中指定的文件描述符是否可以讀寫)。

5》函數select

用法:int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

作用:用來夠監視我們需要監視的文件描述符(讀或寫的文件集中的文件描述符)的狀態變化情況。並能通過返回的值告知我們。

參數解釋:

int maxfdp:集合中所有文件描述符的範圍,爲所有文件描述符的最大值加1。

fd_set *readfds:要進行監視的讀文件集。

fd_set *writefds :要進行監視的寫文件集。

fd_set *errorfds:用於監視異常數據。

struct timeval* timeout:select的超時時間,它可以使select處於三種狀態:

  • 第一,若將NULL以形參傳入,即不傳入時間結構,就是 將select置於阻塞狀態,一定等到監視文件描述符集合中某個文件描述符發生變化爲止;
  • 第二,若將時間值設爲0秒0毫秒,就變成一個純粹的非阻塞函數, 不管文件描述符是否有變化,都立刻返回繼續執行,文件無變化返回0,有變化返回一個正值;
  • 第三,timeout的值大於0,這就是等待的超時時間,即 select在timeout時間內阻塞,超時時間之內有事件到來就返回了,否則在超時後不管怎樣一定返回。
struct timeval timeout; timeout.tv_sec = 0; //秒 timeout.tv_usec = dwTimeout * 1000; //微秒 1毫秒 = 1000微秒

返回值介紹:

>0:被監視的文件描述符有變化,返回對應位仍然爲1的fd的總數。
-1:出錯
0 :超時

舉例:

比如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;
         };
}
發佈了15 篇原創文章 · 獲贊 30 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章