select函數與I/O多路轉接
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函數的這三個參中的任一個(或全部)可以是空指針,這表示對相應的條件不關心。值得一提的是:如果這三個指針全部為空,則select函數提供了比sleep更精確的計時器(sleep等待整數秒,而select函數可以等待少於1秒的時間,具體時間粒度取決於系統時鐘)。
select第一個參數 maxfdp1的意思是“最大的fd加1(max fd plus 1)”。在三個描述符集中找出最大的描述符值,然後加1,這就是第一個參數。也可以將第一個參數設置為FD_SETSIZE,這是<sys/types.h>這義的一個常數,通常是256或1024。但對於大部分應用程序來說,此值太大了。如果將maxfdp1設置為最大的描述符值加1,內核只需要在此範圍內尋找打開位,而不必在上數百個的大範圍內搜索。
如下是示例代碼:
fd_set readset, writeset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0, &readset);
FD_SET(3, &readset);
FD_SET(1, &writeset);
FD_SET(2, &writeset);
select(4, &readset, &writeset, NULL, NULL); //註意第一個參數為4
select有三個可能的返值:
1)返回值-1表示出錯。例在未有描述符準備好數據時捕捉到一個信號時
2)返回值0表示沒有描述符準備好。若指定的描述符都沒有準備好,並且指定的時間已到,則發生這種情況。
3)返回一個正數,說明已經準備好的描述符數,在這種情況下。三個描述符集中仍舊打開的位是已準備好的描述符位。
對於“準備好”的意思,要作一些列具體的說明:
1)對於讀集中的一個描述符的read不會阻塞,則此描述符是準備好的。
2)對於寫集中的一個描述符的write不會阻塞,則此描述符是準備好的。
3)對於異常條件集中的一個描述符有一個未決異常條件,則此描述符是準備好的。
如果在一個描述符中碰到文件結束符,則select認為描述符是可讀的,然後調用read,它返回0,這是unix指示到達文件尾處的方法。
通過select函數實現I/O多路轉接,上面第二個例子的代碼可改寫成如下:
//clientfd[] 為客戶端的socket描述符組數,假設數組的大小為MAX。
//serverfd表示服務器描述符,非阻塞。
//readsocket表示客戶端socket描述集,同樣包括服務的socket描述符
//maxfdp1 表示readsocket中最大 socket值加1
while(1)
{
int n = select(maxfdp1, &readsocekt, NULL, NULL, NULL)
if(n >0)
{
//is that some connectiion request
if(FD_ISSET(serverfd, &readsocket))
{
//用accept函數來獲取連接的客戶socket描述符,並加到客戶端描述符數組clientfd和readsocket中。
}
for(int i = 0; i < MAX; ++i)
{
if(FD_ISSET(clientfd[i], &readsocket))
{
//response to client here.
}
}
}
}
在本例代碼每次循環時,都采用select函數查詢是否有描述符準備好,有則處理。無則阻塞,直到有數據準備好為止。在這段時間裏面,可以讓CPU做其它事情,避免了輪詢方法所占用的大量CPU時間。
最後關於I/O多路轉接問題的情況。I/O多路轉接至今還不是POSIX的組成部分。SVR4和4.3+BSD都提供select函數以執行I/O多路轉接。SVR4實際上用poll實現select。同時,在SVR4和BSD的select實現之間,有些差異,BSD系統總是返回一個所有準備好的描述符數之和,如果某個描述符同時在兩個集中(如讀集和寫集),則返回值把它的描述符中累加兩次。不同的是,SVR4更正了這一點,只計一次。於此,唯有POSIX標準化了select這樣的函數才能解決此問題。
最後,寫本文的初衷是見到網上介紹select的資料不多,而且不夠詳細,故有感而寫。上面的代碼只能用來說明問題,也許表達得不夠清楚。上面對select函數的描述來源於<<UNIX環境高級編程>>(中文版)一書。需要的話可以參考此書,此書不失為一本經典的UNIX書籍。
---------------------
作者:海楓
來源:CSDN
原文:https://blog.csdn.net/linyt/article/details/1722445
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
select函數與I/O多路轉接