五種IO模型(通過例子說明)
通俗地講,在網路環境下IO可分為兩個部分:等待和資料遷移。
如果要提高IO效率,則需要減少等待時間。
五種IO模型分別為:阻塞式IO、非阻塞式IO、訊號驅動IO、多路複用IO及非同步IO。前四個為同步IO。
- 所謂同步,就是在發出一個呼叫時,在沒有得到結果之前,該呼叫就不返回。但是一旦呼叫返回。就得到了返回值;換句話說,就是由呼叫者主動等待這個呼叫的結果。
- 非同步則相反,呼叫發出之後,這個呼叫就直接返回了,所以沒有返回結果;換句話說,當一個非同步過程呼叫發出後,呼叫者不會立刻得到結果;而是在呼叫發出後,被呼叫者通過狀態、通知來通知呼叫者,或通過回撥函式處理這個呼叫。
一、阻塞式IO
阻塞式IO:IO即input/output,阻塞式IO指的是“一旦輸入/輸出工作沒有完成,則程式阻塞,直到輸入/輸出工作完成”。在目前,我們從書本上學到的語法用的基本都是阻塞式IO。比如c語言的stdio.h庫的所有函式(包含scanf(),getchar(),gets()等函式),Java的BIO(比如各類輸入輸出流)。他們都是不見黃河心不死的好漢。在你滿足他們的條件之前,不讓你的程式繼續往下跑。最簡單的例子:c語言的scanf()函式——當你scanf()要求輸入兩個數字時,你只輸入一個數字,它也不會讓你繼續執行接下來的程式碼的。
舉個栗子:A拿著一支魚竿在河邊釣魚,一直在魚竿前等,在等待的過程中啥也不幹,就一心一意等待魚上鉤。只有當魚上鉤的時候,才會結束掉這個等待狀態。
在核心將資料準備好之前,系統呼叫會一直等待所有的套接字,預設的是阻塞方式。
其實,我們例子中所說的魚竿就是這一個檔案描述符。這個模型是我們最常見的,程式呼叫和我們編寫的基本程式是一致的。
fd = connect();
write(fd);
read(fd);
close(fd);
程式的read必須在write之後執行,當write阻塞住了,read就不能執行下去,一直處於等待狀態。
二、非阻塞式IO
非阻塞式IO:非阻塞式IO其實也並非完全非阻塞,通常都是通過設定超時來讀取資料的。未超時之前,程式阻塞在讀寫函式上;超時後,結束本次讀取,將已讀到的資料返回。通過不斷迴圈讀取,就能夠讀到完整資料了。如果多次連續超時讀到空資料的話,則可以斷開。C語言的Socket可以使用setsockopt()來設定recv()超時(通常也就Socket需要考慮超時)。
舉個栗子:B拿著一隻魚竿在河邊釣魚,但是B在等待魚上鉤的過程中,同時也在幹別的事情(例如:玩手機、打遊戲等),但是在幹別的事的時候會每隔一定的時間檢查一下是否有魚上鉤,一旦檢查到有魚上鉤,就會停下手中的事情,把魚釣上來。
其實,B在檢查魚竿是否有魚,是一個輪詢的過程。
每次客戶詢問核心是否有資料準備好,即檔案描述符緩衝區是否就緒。當有資料報準備好時,就進行拷貝資料報的操作。當沒有資料報準備好時,也不阻塞程式,核心直接返回未準備就緒的訊號,等待使用者程式的下一個輪尋。
但是,輪尋對於CPU來說是較大的浪費,一般只有在特定的場景下才使用。
三、訊號驅動IO
訊號驅動IO是指:程序預先告知核心,使得 當某個socketfd有events(事件)發生時,核心使用訊號通知相關程序。
舉個栗子:C拿著一支魚竿在河邊釣魚,但與A、B不同,C在釣魚的同時也幹別的事,不過C在魚竿上掛一個鈴鐺,當有魚上鉤的時候,這個鈴鐺就會響,C就將魚釣上來。
訊號驅動IO模型,應用程序告訴核心:當資料報準備好的時候,給我傳送一個訊號,對SIGIO訊號進行捕捉,並且呼叫我的訊號處理函式來獲取資料報。
四、多路複用IO
多路複用IO實際上就是用select, poll, epoll監聽多個io物件,當io物件有變化(有資料)的時候就通知使用者程序。好處就是單個程序可以處理多個socket。
舉個栗子:D也在河邊釣魚,不過D比較有錢,他有很多根魚竿,同時等待多根魚竿,D不斷檢視每根魚竿是否有魚上鉤。提高了效率,減少了等待時間。
多路複用IO是多了一個select函式,select函式有一個引數是檔案描述符集合,對這些檔案描述符進行迴圈監聽,當某個檔案描述符就緒時,就對這個檔案描述符進行處理。
其中,select只負責等,recvfrom只負責拷貝。 多路複用IO是屬於阻塞IO,但可以對多個檔案描述符進行阻塞監聽,所以效率較阻塞IO的高。
(1)當用戶程序呼叫了select,那麼整個程序會被block;
(2)而同時,kernel會“監視”所有select負責的socket;
(3)當任何一個socket中的資料準備好了,select就會返回;
(4)這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。
所以,I/O 多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回。
這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裡需要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。
所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用多執行緒 + 阻塞 IO的web server效能更好,可能延遲還更大。
select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。
五、非同步IO
舉個栗子:E也想釣魚,但是E有其他事情,抽不開身,就僱了F幫他釣魚,一旦有魚上鉤,F就通知E有魚上鉤了,E再來將魚釣上來。
當應用程式呼叫aio_read時,核心一方面去取資料報內容返回,另一方面將程式控制權還給應用程序,應用程序繼續處理其他事情,是一種非阻塞的狀態。
當核心中有資料報就緒時,由核心將資料報拷貝到應用程式中,返回aio_read中定義好的函式處理程式。
可以看出,阻塞程度:阻塞IO>非阻塞IO>多路轉接IO>訊號驅動IO>非同步IO,效率是由低到高的。