一、五種I/O模型
1、阻塞I/O
我們在前面所說的I/O模型都是阻塞I/O,即調用recv系統調用,如果沒有資料則阻塞等待,當資料到來則將資料從核心空間(套介面緩衝區)拷貝到使用者空間(recv函數提供的buf),然後recv返回,進行資料處理。
2、非阻塞I/O
我們可以使用 fcntl(fd, F_SETFL, flag | O_NONBLOCK); 將通訊端標誌變成非阻塞,調用recv,如果裝置暫時沒有資料可讀就返回-1,同時置errno為EWOULDBLOCK(或者EAGAIN,這兩個宏定義的值相同),表示本來應該阻塞在這裡(would
block,虛擬語氣),事實上並沒有阻塞而是直接返回錯誤,調用者應該試著再讀一次(again)。這種行為方式稱為輪詢(Poll),調用者只是查詢一下,而不是阻塞在這裡死等,這樣可以同時監視多個裝置:
while(1)
{
非阻塞read(裝置1);
if(裝置1有資料到達)
處理資料;
非阻塞read(裝置2);
if(裝置2有資料到達)
處理資料;
..............................
}
如果read(裝置1)是阻塞的,那麼只要裝置1沒有資料到達就會一直阻塞在裝置1的read調用上,即使裝置2有資料到達也不能處理,使用非阻塞I/O就可以避免裝置2得不到及時處理。
非阻塞I/O有一個缺點,如果所有裝置都一直沒有資料到達,調用者需要反覆查詢做無用功,如果阻塞在那裡,作業系統可以調度別的進程執行,就不會做無用功了,在實際應用中非阻塞I/O模型比較少用。
3、I/O複用
用select來管理多個I/O,當沒有資料時select阻塞,如果在逾時時間內資料到來則select返回,再調用recv進行資料的複製,recv返回後處理資料。
4、訊號驅動I/O
先註冊SIGIO訊號的處理函數,進程繼續執行其他動作,當資料到來時會發送SIGIO訊號給進程,然後可以在訊號處理函數中調用recv進行資料的複製,然後recv返回進行資料處理。
5、非同步I/O
aio_read 函數也會提供一個buf,系統調用進入核心,如果沒有資料則立即返回,進程繼續執行其他動作,所以叫非同步I/O,當資料到來時核心自動複製資料,然後推送給使用者空間,通過在aio_read中指定的訊號通知進程,讓其處理資料。非同步I/O跟訊號驅動I/O的不同之處在於,它不用調用recv進行資料的複製,如果將後者比做”拉pull“,則前者可以認為是”push推“,push的效率會高點,其實非同步I/O跟windows下面的完成連接埠差不多,但aio_read的實現或多或少存在問題,用得也比較少。
二、select函數簡介
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
參數1:讀寫異常集合中的檔案描述符的最大值加1;
參數2:讀集合,關心可讀事件;
套介面緩衝區有資料可讀
串連的讀一半關閉,即接收到FIN段,讀操作將返回0
如果是監聽套介面,已完成串連隊列不為空白時。
套介面上發生了一個錯誤待處理,錯誤可以通過getsockopt指定SO_ERROR選項來擷取。
參數3:寫集合,關心可寫事件;
套介面發送緩衝區有空間容納資料。
串連的寫一半關閉。即收到RST段之後,再次調用write操作。
套介面上發生了一個錯誤待處理,錯誤可以通過getsockopt指定SO_ERROR選項來擷取。
參數4:異常集合,關心例外狀況事件;
套介面存在帶外資料(TCP頭部 URG標誌,16位緊急指標欄位)
參數5:逾時時間結構體
對於參數2,3,4來說,如果不關心對應事件則設定為NULL即可。注意5個參數都是輸入輸出參數,即select返回時可能對其進行了修改,比如集合被修改以便標記哪些套介面發生了事件,時間結構體的傳出參數是剩餘的時間,如果設定為NULL表示永不逾時。用select管理多個I/O,select阻塞等待,一旦其中的一個或多個I/O檢測到我們所感興趣的事件,select函數返回,傳回值為偵測到的事件個數,並且返回哪些I/O發送了事件,遍曆這些事件,進而處理事件。注意當select阻塞返回後,此時調用read/write
是不會阻塞的,因為正是有可讀可寫事件發生才導致select 返回,也可以認為是select 提前阻塞了。
下面是4個可以對集合進行操作的宏:
void FD_CLR(int fd, fd_set *set); // 清除出集合
int FD_ISSET(int fd, fd_set *set); // 判斷是否在集合中
void FD_SET(int fd, fd_set *set); // 添加進集合中
void FD_ZERO(fd_set *set); // 將集合清零
select函數的舉例應用看這裡。
參考:
《Linux C 編程一站式學習》
《TCP/IP詳解 卷一》
《UNP》