轉載自: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;
};
}