目錄
一、對IO的重新認識
二、IO的五種模型
1.阻塞IO
2.非阻塞IO
3.信號驅動IO
4.IO多路轉接
5.異步IO
6.一些概念的解釋
三、非阻塞IO的代碼實現
1.fcntl
2.實現主程序
一、對IO的重新認識
如果有人問你IO是什么,你該怎么回答呢?
你可能會說,IO不就是input和output表示輸入和輸出,輸入表示把數據從硬盤等外設拷貝到內存,而輸出表示把數據從內存拷貝到其他外設。
雖然這樣說沒什么大問題,但還不夠深刻。
我們不妨設想下面的現象,有一個進程,調用read/recv這樣的系統調用讀取數據。如果此時讀取條件不滿足,那就沒有數據可供進程讀取,進程只就會一直等待數據準備好。
IO除了拷貝數據需要消耗時間,還包含這個等待的過程所以我們使用的系統調用除了拷貝代碼,也包含了等的這部分代碼。
也就是說,IO=等+數據拷貝
那什么是高效IO呢?
我們知道,IO過程我們在意的是拷貝,而不是等待。而拷貝需要的時間是由電路還有系統實現等保證的。隨著科技的發展,拷貝本身花費的時間已經基本沒有提升空間了,所以拷貝本身的效率已經很難再有提升了。那么等待時間的長度就決定了IO的效率。
換句話說,單位時間內,等待的比重越低,IO效率越高。
二、IO的五種模型
1.阻塞IO
在內核將數據準備好之前,系統調用會一直等待。我們之前寫代碼使用的IO接口讀取文件描述符,默認都使用阻塞IO方式。
下圖就是阻塞IO的示意圖,進程調用recvfrom這樣的IO接口從內核中讀取數據。如果數據沒有準備好,進程就會阻塞在調用處等待,數據準備好后,才會將內核中的數據拷貝到用戶緩沖區,并給出返回值。
阻塞IO是最常見的IO模型,也最簡單,我們之前寫的所有代碼,IO都是阻塞式的。
2.非阻塞IO
如果內核還未將數據準備好,系統調用仍然會直接返回,并且返回EAGAN或者EWOULDBLOCK錯誤碼。
如圖所示,進程調用recvfrom從內核緩沖區中讀取數據。如果數據沒有準備好,就會給進程返回一個EWOULDBLOCK錯誤碼,告訴進程數據還沒準備好,進程就會接著去干自己的事情。
過了一段時間,進程還會調用recvfrom讀取數據,不斷反復,直到數據準備好。接著系統調用完成拷貝并返回成功的返回值。
非阻塞IO需要程序員設計循環代碼,反復嘗試讀寫文件描述符,這個過程稱為輪詢。但輪詢對CPU有一定的性能浪費,只有特定場景下才使用。
3.信號驅動IO
信號驅動IO會在內核將數據準備好的時候,發送SIGIO信號通知進程進行IO操作。
如圖所示,信號驅動IO模型,該模式使用信號處理函數執行IO。
首先使用signal注冊信號處理函數為包含IO系統調用的函數。所以只要進程收到信號,就可以在處理函數中調用recvfrom拷貝已經準備好的數據。
也就是說,只要數據準備完成了,進程就會收到信號,進程直接來拷貝就可以了。其余時間進程還可以繼續執行自己的代碼。
但是我們之前也說過,如果我們給一個進程同時發很多信號,只有兩個能被最終遞達。而這里的信號丟失就相當于讀取次數減少,就相當于數據丟失。所以,這種很少有符合這種模式的IO狀態。
4.IO多路轉接
IO多路轉接可以理解為多個阻塞IO同時進行,并不斷遍歷檢測哪個IO的文件描述符準備好了,準備好了就會執行拷貝。
如圖所示為IO多路轉接模式,它將IO的等待和拷貝分開了。
進程調用select系統調用等待內核中的數據就緒,就緒以后會通知進程調用recvfrom來將數據拷貝到用戶緩沖區中。
由于多路轉接可以同時等待多個文件描述符。所以,當一個或者多個文件的緩沖區中數據就緒時,都會通知上層用戶讀取。而且每個拷貝過程也是并行的,還是免不了等,但是等的比重降低了很多,從而提高了IO的效率。
多路轉接既是效率最高的IO模式,也是我們以后講解的重點。
5.異步IO
當一個異步IO調用發出后,調用者不會立刻得到結果,而是在調用發出后,被調用者通過狀態、信號等來通知調用者,或通過回調函數處理這個調用。
下圖表示異步IO,進程調用aio_read,將等待數據就緒和將數據拷貝到用戶緩沖區兩個步驟的工作全部交給操作系統來完成。當操作系統完成兩個步驟以后,直接通知上層用戶去用戶緩沖區中讀取數據即可。
也就是,進程不需要再考慮數據的IO,而是將其全權交給操作系統完成。
6.一些概念的解釋
什么是同步IO和異步IO?
同步和異步的區別在于消息通信的機制。
所謂同步,就是在發出一個調用時,在沒有得到結果之前,該調用就不返回,但是一旦調用返回,就得到返回值了。
換句話說就是,調用者在主動等待這個調用的結果。
而異步則是調用開始執行后直接返回,調用者不會立刻得到結果,而是在調用返回后,被調用者通過狀態、信號等通知調用者或通過回調函數處理。
話句話說,就是把事情交給了其他應用去做,自己只根據通信數據接收處理結果。
線程同步和同步IO有什么關系?
我們在講解Linux線程時也提到了同步。
線程同步表示多個線程同時對臨界資源進行操作時,系統為了保證沒有線程處于饑餓狀態,會以一定的順序安排各個線程的執行順序。
而同步IO表示處理數據的進程本身是否全權參與IO過程。
也就是,同步IO和線程同步之間,除了都有同步這個詞之外沒有任何關系。
三、非阻塞IO的代碼實現
1.fcntl
int fcntl(int fd, int cmd, ... /* arg */ );
-
頭文件:unistd.h、fcntl.h
-
功能:修改文件描述符的屬性或對其進行其他操作。
-
參數:int fd需要操作的文件描述符。int cmd表示對描述符的操作。...表示可變參數列表,傳入的cmd不同,參數也不同
-
返回值:成功返回非-1的值,失敗返回-1。
fcntl函數有5種功能:
復制一個現有的描述符(cmd=F_DUPFD).
獲得/設置文件描述符標記(cmd=F_GETFD或F_SETFD).
獲得/設置文件狀態標記(cmd=F_GETFL或F_SETFL).
獲得/設置異步I/O所有權(cmd=F_GETOWN或F_SETOWN).
獲得/設置記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).
我們只使用第三個功能,即獲取/設置文件狀態標記,可將一個文件描述符設置為非阻塞。我們寫一個SetNonBlock函數支持該功能。
//將文件描述符設為非阻塞
void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL);//獲取文件描述符的標志,該標志是一個位圖結構if(fl < 0)//獲取失敗{std::cerr << "fctnl:" << strerror(errno) << std::endl;//打印錯誤碼}else{fcntl(fd, F_SETFL, fl | O_NONBLOCK);//將該文件描述符設為非阻塞}
}
2.實現主程序
由于我們從標準輸入流(文件描述符為0)中讀取數據,所以只要我們敲擊鍵盤輸入文字,就相當于向標準輸入流中寫入數據。
main.cc
#include"util.hpp"
#include<iostream>int main()
{SetNonBlock(0);//設置文件描述符為非阻塞while(1){char buffer[1024];ssize_t n = read(0, buffer, sizeof(buffer)-1);//讀取數據if(n > 0)//讀到了數據{buffer[n] = '\0';std::cout << buffer << std::endl;}else if(n == 0)//讀到了結尾{std::cout << "read end" << std::endl;break;}else//n等于-1有兩種情況,一種是讀取出錯,另一種是數據還沒準備好,read只能按-1返回{if (errno == EAGAIN)//錯誤碼為EAGAIN表示數據還沒有準備好{//std::cout << "我沒錯, 只是沒有數據" << std::endl;print_work();//程序繼續執行自己的事}else if (errno == EINTR)//錯誤碼為EINTR表示讀取時進程收到了信號,需要進行處理,讀取就被暫時打斷了。{continue;//繼續循環}else//這次就是出錯了,打印錯誤碼就可以了{std::cout << " errno: " << strerror(errno) << std::endl;break;}}sleep(1);}return 0;
}