1. 程式人生 > >select函數與I/O多路轉接

select函數與I/O多路轉接

error strong pan 實現 tail 問題 exce 準備就緒 這樣的

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多路轉接