轉自:http://blog.csdn.net/linyt/article/details/1722445
select函數與I/O多路轉接
相作大家都寫過讀寫IO操作的代碼,例如從socket中讀取資料可以使用如下的代碼:
while( (n = read(socketfd, buf, BUFSIZE) ) >0)
if( write(STDOUT_FILENO, buf, n) = n)
{
printf(“write error”);
exit(1);
}
當代碼中的socketfd描述符所對應的檔案表項是處於阻塞時,它會一直阻塞,直到有資料從網路的另一端發送過來。如果它是一個伺服器程式,它要讀寫大量的socket,那麼在某一個socket上的阻塞很明顯會影響與其它socket的互動過程。類似的問題不單單出現在網路上,還可以出現在讀寫加鎖的檔案和FIFO等等一系列的情況。
一種比較好的解決方案似乎是採用非阻塞IO來實現。把所要讀取資料的socketfd設定為非阻塞狀態,依次用read函數檢查是否有資料到來,如有,它會返回接到資料的個數,否則它會返回-1以表示當前還沒有資料到達。這樣,對於每個socket,如有資料到來則讀取,沒有也會馬上返回。這就是非阻塞IO的好處拉。部分代碼如下:
//clientfd[] 為用戶端的socket描述符組數,假設數組的大小為MAX,並且所有用戶端socket描述符都設定為非阻塞狀態時。
for(i = 0; i < MAX; ++i)
{
int n;
if( (n = read(clientfd[i], buf, SZIE)) >0)
{
//send response to client in here.
}
}
這裡代碼看起來與上面的代碼沒有太大的區別,其實是有很大的區別;區別就是使使用了非阻塞IO進行整個互動過程,使得各個用戶端都得到相對平等的時間待遇。這種模式我們通常稱為這“輪詢”模式。輪詢模式同樣有它的不足之處,在執行read函數時,實際上大部分時間還是沒有資料可讀的,但仍不斷地執行read,浪費了很多CPU時間。
實際,對於上述的問題,一種比較好的技術就是I/O多路轉接(I/O multiplexing)。它可謂是上面兩種方法的接衷:先構造一張有關描述符的資料表,然後調用一個函數,僅當有一個或多個描述符已準備可以進行IO操作時才返回,否則一直阻塞。在返回時,它會告訴進程那些描述符已準備好可以進行IO。
現在實現多路轉接的任務落在select函數的身上了,現在給大家詳細介紹select函數的使用。我們的主角出場了,呵呵。掌聲。
函數的功能:實現多路轉接,通過調用核心來實現。它向核心提供如下的參數
1)我們所關心的描述符
2)對於每個描述符,我們所關心的條件(是否讀一個給定的描述符,還是想寫一個給定的描述符,還是關心一個描述符的異常條件)
3)希望等待多久時間(可以永遠等待,等待一個固定時間,或完全不等待)
從select返回時,核心告訴我們:
1)已準備好的描述符數量
2)哪一個描述符已準備好讀、寫或異常條件
使用這種傳回值,就可調用相應的I/O函數,通常是read或write,並確知該函數不會阻塞。
函數的定義:
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
int select( int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, sturct timeval *tvptr);
返回:準備就緒的描述符,若逾時則為0,若出錯則為-1
最後一個參數為struct timeval的指標變數,它指定願意等待的時間。
struct timeval{
long tv_sec; /*秒數*/
long tv_usec; /*微秒數*/
};
對於參數tvptr有三種情況:
如果tvptr == NULL 則永遠等待。如果捕捉到一個訊號則中斷此無限期等待。當指定的描述符中的一個或多個已準備好或捕捉到一個訊號則返回。如果是捕捉到一個資訊,則select返回-1,errno設定為EINTR.
如果tvptr->sec ==0 && tvptr->tv_usec == 0 則完全不等待。即測試所有的描述符後馬上返回。這是得到多個描述符的狀態而不阻塞select函數的輪詢方法。
如果tvptr->tv_sec != 00 || tvptr->tv_usec != 0 則等待指定的秒數和微秒數。當指它的描述符之一已準備好,或指定的時間值已逾時則返回。如果在逾時時還沒有一個描述符準備好,則傳回值是0。與第一種情況類似,這種等待可能被訊號所中斷。
中間三個參數readfds, writefds, exceptfds是指向描述符集的指標,它們描述了我們關心的可讀、可寫和處異常條件的各個描述符。這種描述符集存在一種叫fd_set的資料類型中(在標頭檔select.h中有定義)。具體做法每個描述符對應於資料結構fd_set所佔用記憶體空間的一個位,如果第i位為0則表示值為i的描述符不包含在該集中,反之亦然。為了方便使用者使用,系統提供了如下的四個宏進行操作。
FD_ZERO(fd_set *fdset); //清空fdset中的所有位
FD_SET(int fd, fd_set *fdset); //在fdset中開啟fd所對應的位
FD_CLR(int fd, fd_set *fdset); //在fdset中關閉fd所對應的位
FD_ISSET(int fd, fd_set *fdset); //測試fd是否在fdset中
通常做法是,先定義一個描述符集
fd_set rset;
int fd;
必須使用FD_ZERO清除其所有位
FD_ZERO(&rset);
然後設定我們所關心的位
FD_SET(fd, &rset);
FD_SET(STDOUT_FILENO,&rset);
從select返回時,用FD_ISSET測試該集中的一個給定位是否仍舊設定
if( FD_ISSET(fd, &rset)){
...
}
select函數的這三個參中的任一個(或全部)可以是null 指標,這表示對相應的條件不關心。值得一提的是:如果這三個指標全部為空白,則select函數提供了比sleep更精確的計時器(sleep等待整數秒,而select函數可以等待少於1秒的時間,具體時間粒紋取決於系統時鐘)。
select第一個參數 maxfdp1的意思是“最大的fd加1(max fd plus 1)”。在三個描述符集中找出最大的描述符值,然後加1,這就是第一個參數。也可以將第一個參數設定為FD_SETSIZE,這是<sys/types.h>這義的一個常數,通常是256或1024。但對於大部分應用程式來說,此值太大了。如果將maxfdp1設定為最大的描述符值加1,核心只需要在此範圍內尋找開啟位,而不必在上數百個的大範圍內搜尋。
如下是範例程式碼: