Unix下可用的5種I/O模型:
阻塞I/O
非阻塞I/O
I/O復用(select和poll)
信號驅動I/O(SIGIO)
異步I/O(POSIX的aio_系列函數)
一個輸入操作通常包括兩個不同的階段:
1)等待數據准備好;
2)從內核向進程復制數據;
對於一個套接字的輸入操作,第一步通常涉及等待數據從網絡中到達。當所等待分組到達時,它被復制到內核中某個緩沖區。第二步就是把數據從內核緩沖區復制到應用進程緩沖區。
阻塞I/O
最流行的I/O模型是阻塞式I/O(blocking I/O) 模型,默認情況下,所有的套接字都是阻塞的。阻塞調用是指調用結果返回之前,當前線程會被掛起(線程進入非可執行狀態,在這個狀態下,cpu不會給線程分配時間片,即線程暫停運行)。函數只有在得到結果之後才會返回。
以數據包套接字為例,如圖
進程調用recvfrom,其系統調用直到數據報到達且被拷貝到應用進程的緩沖區或者發生錯誤才返回。最常見的錯誤是系統調用被信號中斷。我們說進程從調用recvfrom開始到它返回的整段時間內是被阻塞的,recvfrom成功返回後,進程開始處理數據報。
非阻塞I/O
非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。
進程把一個套接口設置成非阻塞是在通知內核:當所請求的I/O操作非得把本進程投入睡眠才能完成時,不要把本進程投入睡眠,而是返回一個錯誤。
前三次調用recvfrom 時沒有數據可返回,因此內核轉而立即返回一個EWOULDBLOCK 錯誤。第四次調用 recvfrom 時已有一個數據報准備好,它被復制到應用程序緩沖區,於是recvfrom 成功返回。我們接著處理數據。
當一個應用進程像這樣對一個非阻塞描述符循環調用 recvfrom 時,我們稱之為輪詢(polling)。應用程序持續輪詢內核,以查看某個操作是否就緒。這樣做往往耗費大量CPU 時間。
I/O復用
主要可以調用select和epoll;對一個IO端口,兩次調用,兩次返回,比阻塞IO並沒有什麼優越性;關鍵是能實現同時對多個IO端口進行監聽,可以等待多個描述符就緒;
I/O復用模型會用到select、poll、epoll函數,這幾個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操作函數
信號驅動I/O模型
我們也可以用信號,讓內核在描述字就緒時發送SIGIO信號通知我們。我們稱這種模型為信號驅動I/O(signal-driven I/O)。
我們首先開啟套接口的信號驅動I/O功能,並通過sigaction系統調用安裝一個信號處理函數。該系統調用立即發回,我們的進程繼續工作,也就是說它沒有被阻塞。當數據報准備好時,內核就為該進程產生一個SIGIO信號。我們隨後既可以在信號處理函數中調用recvfrom讀取數據報,並通知主循環數據已經准備好待處理,也可以立即通知主循環,讓它讀取數據報。
無論如何處理SIGIO信號,這種模型的優勢在於等待數據報到達期間,進程不被阻塞。主循環可以繼續執行,只要不時等待來自信號處理函數的通知:既可以是數據已經准備好被處理,也可以是數據報已准備好被讀取。
異步I/O模型
異步I/O(asynchronous I/O)有POSIX規范定義。後來演變成當前POSIX規范的各種早期標准定義的實時函數中存在的差異已經取得一致。一般地說,這些函數的工作機制是:告知內核啟動某個操作,並讓內核在整個操作(包括將數據從內核拷貝到我們自己的緩沖區)完成後通知我們。這種模型與前與前面介紹的信號驅動模型的主要區別在於:信號驅動I/O是由內核通知我們何時可以啟動一個I/O操作,而異步I/O模型是由內核通知我們I/O操作何時完成。
各種模型的比較
可以看出,前4種模型的主要區別在於第一階段,因為它們的第二階段是一樣的:在數據從內核復制到調用者的緩沖區起見,進程阻塞與recvfrom 調用,相反。異步I/O模型在這兩個階段都需要處理,從而不同於其他四種模型。
同步I/O與異步I/O對比
POSIX把這兩個術語定義如下:
·同步I/O操作(synchronous I/O operation)導致請求進程阻塞,直到I/O操作完成。
·異步I/O(asynchronous I/O operation)不導致請求進程阻塞。
根據上述定義,我們前4種模型----阻塞I/O模型、非阻塞I/O模型、I/O復用模型和信號去驅動I/O模型都是同步I/O模型,因為其中真正的I/O操作(recvfrom)將阻塞進程。只有異步I/O模型與POSIX定義的異步I/O相匹配。
select 函數
該函數允許進程指示內核等待多個事件中的任何一個發生,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。
作為一個例子,我們可以調用select,告知內核僅在下列情況發生時才返回:
1)集合{ 1, 4, 5 } 中任何描述符准備好讀;
2)集合{ 2, 7 } 中任何描述符准備好寫;
3)集合{ 1, 4 } 中任何描述符有異常條件待處理;
也就是說,我們調用 select 告知內核對哪些描述符(就讀、寫或異常條件)感興趣以及等待多長時間。我們感興趣的描述符不局限於套接字,任何描述符都可以用select 來測試。函數描述如下:
view plaincopy
#include
#include
intselect(intmaxfdp1,fd_set*readset,fd_set*writeset,fd_set*exceptset,
conststructtimeval*timeout);
從最後一個參數timeout 開始介紹,它告知內核等待所指定描述符中任何一個就緒可花多長時間。其timeval結構用於指定這段時間的秒數和微妙數。 view plaincopy
structtimeval
{
longtv_sec;//seconds
longtv_usec;//mircoseconds
}
這個參數有以下三種可能:
1)永遠的等待下去:僅在有一個描述符准備好I/O時才返回。為此,我們把這個參數設置為空指針;
2)等待一段固定時間:在有一個描述符准備好I/O時返回,但是不超過由該參數所指向的timeval 結構中指定的秒數和微秒數;
3)根本不等待:檢查描述符後立即反悔,這稱為輪詢(polling)。為此,該參數必須指向一個timeval結構,而且其中的定時器值(由該結構指定的秒數和微秒數)必須為0;
中間的三個參數 readset 、writeset 和 exceptset 指定我們要讓內核測試讀、寫和異常條件的描述符。
select 使用描述符集,通常是同一個整數數組,其中每個整數中的每一位對於一個描述符。舉例來說,假設使用32位整數,那麼該數組的每一個元素對應於描述符0~31,第二位元素對應於描述符32~63,依次類推, 它們隱藏 為 fd_set 的數據類型和以下四個宏中:
view plaincopy
voidFD_ZERO(fd_set*fdset);//從fdset中清除所有的文件描述符
voidFD_SET(intfd,fd_set*fdset);//將fd加入到fdset
voidFD_CLR(intfd,fd_set*fdset);//將fd從fdset裡面清除
intFD_ISSET(intfd,fd_set*fdset);//判斷fd是否在fdset集合中
舉個例子,以下代碼用於定義一個fd_set 類型的變量,然後打開描述符 1、4 和 5 的對應位;
view plaincopy
fd_setrset;
FD_ZERO(&rset);
FD_SET(1,&rset);
FD_SET(4&rset);
FD_SET(5,&rset);
描述符集的初始化非常重要,因為作為自動變量分配的一個描述符集如果沒有初始化,那麼可能發生不可預期的後果。
select 函數修改由指針 readset 、writeset 和 exceptset 所指向的描述符集,因而這三個參數都是值-結果參數。調用該函數時,我們指定所關心的描述符的值,該函數返回時,結果將指示哪些描述符就緒。該函數返回後,我們使用FD_ISSET宏測試 fd_set 數據類型中的描述符。描述符集內任何與未就緒描述符對應的位返回時均清0。為此,每次重新調用select函數時,我們都得再次把所以描述符集內所關心的為均置一。
數的返回值表示跨所有描述符集的已就緒的總位數。如果任何描述符就緒之前定時器到時,那麼返回0.返回-1表示出錯。
描述符就緒條件:
對於可讀文件描述符集以下四種情況會導致置位:
1、socket接收緩沖區中的數據量大於或等於當前緩沖區的低水位線.此時對於read操作不會被阻塞並且返回一個正值(讀取的字節數).低水位線可以通過SO_RCVLOWAT選項設定,對於Tcp和Udp來說其默認值為1.
2、socket連接的讀端被關閉,如shutdown(socket, SHUT_RD)或者close(socket).對應底層此時會接到一個FIN包,read不會被阻塞但會返回0.代表讀到socket末端.
3、socket是一個監聽socket並且有新連接等待.此時accept操作不會被阻塞.
4、發生socket錯誤.此時read操作會返回SOCKET_ERROR(-1).可以通過errno來獲取具體錯誤信息.
對於可寫文件描述符集以下四種情況會導致置位:
1、socket發送緩沖區中的可用緩沖大小大於或等於發送緩沖區中的低水位線並且滿足以下條件之一
(1)、socket已連接
(2)、socket本身不要求連接,典型如Udp
低水位線可以通過SO_SNDLOWAT選項設置.對於Tcp和Udp來說一般為2048.
2、socket連接的寫端被關閉,如shutdown(socket, SHUT_WR)或者close(socket).在一個已經被關閉寫端的句柄上寫數據會得到SIGPIPE的信號(errno).
3、一個非阻塞的connect操作連接成功 或者 connect操作失敗.
4、發生socket錯誤.此時write操作會返回SOCKET_ERROR(-1).可以通過errno來獲取具體錯誤信息.
對於異常文件描述符集只有一種情況(針對帶外數據):
當收到帶外數據(out-of-band)時或者socket的帶外數據標志未被清除.
下面看個具體例子:
server
view plaincopy
#include
#include
#include
#include
#include
#include
#include
#include
#include
#definePORT8888
#defineMAXSIZE128
intmain()
{
inti,nbyte;
intlistenfd,confd,maxfd;
charbuffer[MAXSIZE];
fd_setglobal_rdfs,current_rdfs;
structsockaddr_inaddr,clientaddr;
intaddrlen=sizeof(structsockaddr_in);
intcaddrlen=sizeof(structsockaddr_in);
if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
perror("socketerror");
exit(-1);
}
else
{
printf("socketsuccessfully!\n");
printf("listenfd:%d\n",listenfd);
}
memset(&addr,0,addrlen);
addr.sin_family=AF_INET;
addr.sin_port=htons(PORT);
addr.sin_addr.s_addr=htonl(INADDR_ANY);
if(bind(listenfd,(structsockaddr*)&addr,addrlen)==-1)
{
perror("binderror");
exit(-1);
}
else
{
printf("bindsuccessfully!\n");
printf("listenport:%d\n",PORT);
}
if(listen(listenfd,5)==-1)
{
perror("listenerror");
exit(-1);
}
else
{
printf("listening...\n");
}
maxfd=listenfd;
FD_ZERO(&global_rdfs);
FD_SET(listenfd,&global_rdfs);
while(1)
{
current_rdfs=global_rdfs;
if(select(maxfd+1,¤t_rdfs,NULL,NULL,0)<0)
{
perror("selecterror");
exit(-1);
}
for(i=0;i<=listenfd+1;i++)
{
if(FD_ISSET(i,¤t_rdfs))
{
if(i==listenfd)
{
if((confd=accept(listenfd,(structsockaddr*)&clientaddr,&caddrlen))==-1)
{
perror("accepterror");
exit(-1);
}
else
{
printf("Connectfrom[IP:%sPORT:%d]\n",
inet_ntoa(clientaddr.sin_addr),clientaddr.sin_port);
FD_SET(confd,&global_rdfs);
maxfd=(maxfd>confd?maxfd:confd);
}
}
else
{
if((nbyte=recv(i,buffer,sizeof(buffer),0))<0)
{
perror("recverror");
exit(-1);
}
elseif(nbyte==0)
{
close(i);
FD_CLR(i,&global_rdfs);
}
else
{
printf("recv:%s\n",buffer);
send(i,buffer,sizeof(buffer),0);
}
}
}
}
}
return0;
}
執行結果如下:
view plaincopy
fs@ubuntu:~$cdqiang/select/
fs@ubuntu:~/qiang/select$./select2
socketsuccessfully!
listenfd:3
bindsuccessfully!
listenport:8888
listening...
Connectfrom[IP:192.168.3.51PORT:1992]
recv:hello
Connectfrom[IP:192.168.3.53PORT:2248]