數據的拷貝是從硬件設備拷到我們應用程序開辟的緩存buffer中,寫的方式就是反過來。其實數據不是直接由硬件設備上拷到我們應用程序開辟的那段緩存之中,其中要經過內核開辟的一段緩存。然后再拷貝到我們的應用程序開辟的緩存之中。而我們的io模型主要就是針對這兩個緩存的階段來進行操作的。
其中只有io多路復用是可以針對多個設備的其他四個都是針對單一設備而進行的。
1.阻塞I/O 模式
例如UDP函數recvfrom的內核到應用層、應用層到內核的調用過程是這樣的:首先把描述符、接受數據緩沖地址、大小傳遞給內核,但是如果此時該與該套接口相應的緩沖區沒有數據,這個時候就recvfrom就會卡(阻塞)在這里,知道數據到來的時候,再把數據拷貝到應用層,也就是傳進來的地址空間,如果沒有數據到來,就會使該函數阻塞在那里,這就叫做阻塞I/O模型。這種模型利用一個進程很無法實現多路IO同時跑。(應用程序調用函數之后,就等待內核準備好數據。內核從硬件設備上等待數據讀取。如果是網絡的哈要等待數據從網絡上傳遞過來。當數據從外設拷到內核的緩存區之后。就再將數據從內核拷到我們應用程序的緩存區中.在這兩個階段中,數據準備階段和從內核拷到應用程序的緩存。我們的都在等待當中。程序沒有運行一直阻塞在哪里)
提高:
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
2解釋:每個TCP套接口有一個發送緩沖區,可以用SO_SNDBUF套接口選項來改變這一緩沖區的大小。當應用進程調用write往套接口寫數據時,內核從應用進程緩沖區中拷貝所有數據到套接口的發送緩沖區,如果套接口發送緩沖區容不下應用程序的所有數據,或者是應用進程的緩沖區大于套接口的發送緩沖區,或者是套接口的發送緩沖區中有別的數據,應用進程將被掛起。內核將不從write返回。直到應用進程緩沖區中的所有數據都拷貝到套接口發送緩沖區。所以,從寫一個TCP套接口的write調用成功返回僅僅表示我們可以重新使用應用進程緩沖區,它并不是告訴我們對方收到數據。TCP發給對方的數據,對方在收到數據時必須給于確認,只有在收到對方的確認時,本方TCP才會把TCP發送緩沖區中的數據刪除。
UDP因為是不可靠連接,不必保存應用進程的數據拷貝,應用進程中的數據在沿協議棧向下傳遞時,以某種形式拷貝到內核緩沖區,當數據鏈路層把數據傳出后就把內核緩沖區中數據拷貝刪除。因此它不需要一個發送緩沖區。寫UDP套接口的write返回表示應用程序的數據或數據分片已經進入鏈路層的輸出隊列,如果輸出隊列沒有足夠的空間存放數據,將返回錯誤ENOBUFS.)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2.非阻塞模式I/O
非阻塞方式實質上是用戶高速內核,如果你沒準備好就立刻返回,別把我阻塞。但如果內核看到數據已經就緒,read執行還是阻塞方式的。注意在6.2圖上要突出非阻塞方式之所以效率低,在于會在內核態和用戶態之間反復進出。所以引入第三點多路復用。(數據在第一階段,準備數據的階段由原來的等待轉變成了過一段時間問一下我們的內核是否準備完成。以輪詢的形式來操作第一階段,第二階段還是相似的)
普通的進程間通信我們使用open函數的時候我們直接在open函數中設定。
O_NONBLOCK 以不可阻斷的方式打開文件, 也就是無論有無數據讀取或等待, 都會立即返回進程之中.
O_NDELAY 同O_NONBLOCK.
而對于網絡的狀況我們使用fcntl函數來修改文件描述符本身激活他的非阻塞功能。
fcntl()函數 當你一開始建立一個套接字描述符的時候,系統內核將其設置為阻塞IO模式。 可以使用函數fcntl()設置一個套接字的標志為O_NONBLOCK 來實現非阻塞。
int fcntl(int fd, int cmd, long arg);
int flag;
flag = fcntl(sockfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flag);
3.多路復用I/O
后講到多路IO復用時強調和非阻塞IO的對比。將retry的邏輯放到了內核態,用戶態采用注冊后等通知的模式。避免用戶態和內核態的反復切換。其主要的意思就是可以同時等待多個設備,只要其中有一個設備準備完畢就可以執行。
注:select等待仍然是阻塞的,而且后繼的read一般也是阻塞的方式。但由于此時read必定能讀出數據,阻塞的時間也不會很長,可以一定程度上實現一些并行的效果。我們后面的實驗可以看到這個效果 。
多路復用是依靠我們的select函數來完成的,在本文的后會講解以下select的用法。
4. 信號驅動I/O模型
當內核為我們準備好數據的時候,就會發送 SIGIO 信號,可以調用 signal注冊SIGIO信號的處理函數,這個時候就可以在 SIGIO 信號處理函數中進行 recvfrom 函數來接受數據報。
這個需要內核的配合,就是我們等待內核從外部設備接受數據的這個部分有我們的內核本身完成當內核準備好就會發個信號給我們應用程序,這時候我們在處理這部分信息。
(將第一階段進一步改進將原有的一次次訪問的模式改為有內核發出信號,再由我們的函數調用內核將數據拷貝到應用程序的緩存)
5. 異步I/O模型
是讓內核拷貝完之后通知我們。信號驅動I/O是當內核準備好數據的時候,通知我們可以調用recvfrom了,而異步I/O模型是內核通知我們I/O操作完成的時候通知我們。類比上面四中IO模型,只有這種可以稱之為真正的異步IO,即用戶發出IO請求后不用管,完全由內核并行執行所有操作,包括數據從內核態到用戶態的拷貝。(將第一和第二階段同時優化,由內核完成第一和第二階段,后直接將數據遞交到我們應用程序的緩存中)
6. Select函數的使用
int select(int maxfdl,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct ti
多端口復用函數select在調用前要首先設置監聽的端口數目,FD_ZERO是清空端口集,FD_SET是設置端口集。
select()函數常常用在用一個進程監聽多個服務器端socket。
有時,select()也被當作延時函數使用。sleep()延時會釋放CPU,select()的話,可以在占用CPU的情況下延時。
select()函數主要是建立在fd_set類型的基礎上的。fd_set(它比較重要所以先介紹一下)是一組文件描述字(fd)的集合,它用一位來表示一個fd(下面會仔細介紹),對于fd_set類型通過下面四個宏來操作:
void FD_ZERO(fd_set *fdset)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)
[cpp] view plain copy
1. fd_set set;
2. FD_ZERO(&set); /*將set清零使集合中不含任何fd*/
3. FD_SET(fd, &set); /*將fd加入set集合*/
4. FD_CLR(fd, &set); /*將fd從set集合中清除*/
5. FD_ISSET(fd, &set); /*測試fd是否在set集合中*/
過去,一個fd_set通常只能包含<32的fd(文件描述字),因為fd_set其實只用了一個32位矢量來表示fd; 現在,UNIX系統通常會在頭文件
[cpp] view plain copy
1. fd_set set;
2. FD_ZERO(&set); /*將set的所有位置0,如set在內存中占8位則將set置為00000000*/
3. FD_SET(0, &set); /*將set的第0位置1,如set原來是00000000,則現在變為100000000,這樣fd==1的文件描述字就被加進set中了*/
4. FD_CLR(4, &set); /*將set的第4位置0,如set原來是10001000,則現在變為10000000,這樣fd==4的文件描述字就被從set中清除了*/
5. FD_ISSET(5, &set); /*測試set的第5位是否為1,如果原來set是10000100,則返回非零,表明fd==5的文件描述符在set中,否則返回0*/
注意:fd的大值必須
select函數的接口比較簡單:
[cpp] view plain copy
1. int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
功能:
測試指定的fd可讀?可寫?有異常條件待處理?
參數:
nfds: 需要檢查的文件描述字個數(即檢查到fd_set的第幾位),數值應該比三組fd_set中所含的大fd值更大,一般設為三組fd_set中所含的大fd值加1(如在readset, writeset, exceptset中所含大的fd為5,則nfds=6,因為fd是從0開始的 )。設這個值是為了提高效率,使函數不必檢查fd_set的所有1024位。
readset: 用來檢查可讀性的一組文件描述字。
writeset: 用來檢查可寫性的一組文件描述字。
exceptset: 用來檢查是否有異常條件出現的文件描述字。(注:錯誤不包括在異常條件之內)
timeout: 有三種可能:
1. timeout = NULL (阻塞:直到有一個fd位被置為1函數才返回)
2. timeout所指向的結構設為非零時間(等待固定時間:有一個fd位被置為1或者時間耗盡,函數均返回)
3. timeout所指向的結構,時間設為0(非阻塞:函數檢查完每一個fd后立即返回)
返回值:返回對應位仍然為1的fd的總數。
Remark:
三組fd_set均將某些fd位置0,只有那些可讀,可寫以及有異常條件待處理的fd位仍然為1。
使用select函數的過程一般是:
先調用宏FD_ZERO將指定的fd_set清零,然后調用宏FD_SET將需要測試的fd加入fd_set,接著調用函數select測試fd_set中的所有fd,后用宏FD_ISSET檢查某個fd在函數select調用后,相應位是否仍然為1。