四種主要的IO模型

  • 2019 年 12 月 19 日
  • 筆記

四種主要的IO模型

伺服器端編程,經常需要構造高性能的網路應用,需要選用高性能的IO模型,這也是通關大公司面試必備的知識。

同步阻塞IO(Blocking IO)

首先,解釋一下這裡的阻塞與非阻塞: 阻塞IO,指的是需要內核IO操作徹底完成後,才返回到用戶空間執行用戶的操作。阻塞指的是用戶空間程式的執行狀態。傳統的IO模型都是同步阻塞IO。在Java中,默認創建的socket都是阻塞的。 其次,解釋一下同步與非同步: 同步IO,是一種用戶空間與內核空間的IO發起方式。同步IO是指用戶空間的執行緒是主動發起IO請求的一方,內核空間是被動接受方。非同步IO則反過來,是指系統內核是主動發起IO請求的一方,用戶空間的執行緒是被動接受方。

在Java應用程式進程中,默認情況下,所有的socket連接的IO操作都是同步阻塞IO(Blocking IO)。 在阻塞式IO模型中,Java應用程式從IO系統調用開始,直到系統調用返回,在這段時間內,Java進程是阻塞的。返回成功後,應用進程開始處理用戶空間的快取區數據。

舉個例子,在Java中發起一個socket的read讀操作的系統調用,流程大致如下:

(1)從Java啟動IO讀的read系統調用開始,用戶執行緒就進入阻塞狀態。

(2)當系統內核收到read系統調用,就開始準備數據。一開始,數據可能還沒有到達內核緩衝區(例如,還沒有收到一個完整的socket數據包),這個時候內核就要等待。

(3)內核一直等到完整的數據到達,就會將數據從內核緩衝區複製到用戶緩衝區(用戶空間的記憶體),然後內核返回結果(例如返回複製到用戶緩衝區中的位元組數)。

(4)直到內核返回後,用戶執行緒才會解除阻塞的狀態,重新運行起來。

總之,阻塞IO的特點是:在內核進行IO執行的兩個階段,用戶執行緒都被阻塞了。

阻塞IO的優點是:應用的程式開發非常簡單;在阻塞等待數據期間,用戶執行緒掛起。在阻塞期間,用戶執行緒基本不會佔用CPU資源。

阻塞IO的缺點是:一般情況下,會為每個連接配備一個獨立的執行緒;反過來說,就是一個執行緒維護一個連接的IO操作。在並發量小的情況下,這樣做沒有什麼問題。但是,當在高並發的應用場景下,需要大量的執行緒來維護大量的網路連接,記憶體、執行緒切換開銷會非常巨大。因此,基本上阻塞IO模型在高並發應用場景下是不可用的。

其次,解釋一下同步與非同步: 同步IO,是一種用戶空間與內核空間的IO發起方式。同步IO是指用戶空間的執行緒是主動發起IO請求的一方,內核空間是被動接受方。非同步IO則反過來,是指系統內核是主動發起IO請求的一方,用戶空間的執行緒是被動接受方。

同步非阻塞IO(Non-blocking IO)

非阻塞IO,指的是用戶空間的程式不需要等待內核IO操作徹底完成,可以立即返回用戶空間執行用戶的操作,即處於非阻塞的狀態,與此同時內核會立即返回給用戶一個狀態值。 簡單來說:阻塞是指用戶空間(調用執行緒)一直在等待,而不能幹別的事情;非阻塞是指用戶空間(調用執行緒)拿到內核返回的狀態值就返回自己的空間,IO操作可以干就干,不可以干,就去干別的事情。 非阻塞IO要求socket被設置為NONBLOCK。 強調一下,這裡所說的NIO(同步非阻塞IO)模型,並非Java的NIO(New IO)庫。

socket連接默認是阻塞模式,在Linux系統下,可以通過設置將socket變成為非阻塞的模式(Non-Blocking)。使用非阻塞模式的IO讀寫,叫作同步非阻塞IO(None Blocking IO),簡稱為NIO模式。在NIO模型中,應用程式一旦開始IO系統調用,會出現以下兩種情況:

(1)在內核緩衝區中沒有數據的情況下,系統調用會立即返回,返回一個調用失敗的資訊。

(2)在內核緩衝區中有數據的情況下,是阻塞的,直到數據從內核緩衝複製到用戶進程緩衝。複製完成後,系統調用返回成功,應用進程開始處理用戶空間的快取數據。

舉個例子。發起一個非阻塞socket的read讀操作的系統調用,流程如下: (1)在內核數據沒有準備好的階段,用戶執行緒發起IO請求時,立即返回。所以,為了讀取到最終的數據,用戶執行緒需要不斷地發起IO系統調用。 (2)內核數據到達後,用戶執行緒發起系統調用,用戶執行緒阻塞。內核開始複製數據,它會將數據從內核緩衝區複製到用戶緩衝區(用戶空間的記憶體),然後內核返回結果(例如返回複製到的用戶緩衝區的位元組數)。 (3)用戶執行緒讀到數據後,才會解除阻塞狀態,重新運行起來。也就是說,用戶進程需要經過多次的嘗試,才能保證最終真正讀到數據,而後繼續執行。 同步非阻塞IO的特點:應用程式的執行緒需要不斷地進行IO系統調用,輪詢數據是否已經準備好,如果沒有準備好,就繼續輪詢,直到完成IO系統調用為止。 同步非阻塞IO的優點:每次發起的IO系統調用,在內核等待數據過程中可以立即返回。用戶執行緒不會阻塞,實時性較好。 同步非阻塞IO的缺點:不斷地輪詢內核,這將佔用大量的CPU時間,效率低下。 總體來說,在高並發應用場景下,同步非阻塞IO也是不可用的。一般Web伺服器不使用這種IO模型。這種IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO這一特性。在Java的實際開發中,也不會涉及這種IO模型。 這裡說明一下,同步非阻塞IO,可以簡稱為NIO,但是,它不是Java中的NIO,雖然它們的英文縮寫一樣,希望大家不要混淆。Java的NIO(New IO),對應的不是四種基礎IO模型中的NIO(None Blocking IO)模型,而是另外的一種模型,叫作IO多路復用模型(IO Multiplexing)。

IO多路復用(IO Multiplexing)

即經典的Reactor反應器設計模式,有時也稱為非同步阻塞IO, Java中的Selector選擇器和Linux中的epoll都是這種模型。

如何避免同步非阻塞IO模型中輪詢等待的問題呢?這就是IO多路復用模型。

在IO多路復用模型中,引入了一種新的系統調用,查詢IO的就緒狀態。在Linux系統中,對應的系統調用為select/epoll系統調用。通過該系統調用,一個進程可以監視多個文件描述符,一旦某個描述符就緒(一般是內核緩衝區可讀/可寫),內核能夠將就緒的狀態返回給應用程式。隨後,應用程式根據就緒的狀態,進行相應的IO系統調用。

目前支援IO多路復用的系統調用,有select、epoll等等。select系統調用,幾乎在所有的作業系統上都有支援,具有良好的跨平台特性。epoll是在Linux 2.6內核中提出的,是select系統調用的Linux增強版本。

在IO多路復用模型中通過select/epoll系統調用,單個應用程式的執行緒,可以不斷地輪詢成百上千的socket連接,當某個或者某些socket網路連接有IO就緒的狀態,就返回對應的可以執行的讀寫操作。

舉個例子來說明IO多路復用模型的流程。發起一個多路復用IO的read讀操作的系統調用,流程如下:

(1)選擇器註冊。在這種模式中,首先,將需要read操作的目標socket網路連接,提前註冊到select/epoll選擇器中,Java中對應的選擇器類是Selector類。然後,才可以開啟整個IO多路復用模型的輪詢流程。

(2)就緒狀態的輪詢。通過選擇器的查詢方法,查詢註冊過的所有socket連接的就緒狀態。通過查詢的系統調用,內核會返回一個就緒的socket列表。當任何一個註冊過的socket中的數據準備好了,內核緩衝區有數據(就緒)了,內核就將該socket加入到就緒的列表中。當用戶進程調用了select查詢方法,那麼整個執行緒會被阻塞掉。

(3)用戶執行緒獲得了就緒狀態的列表後,根據其中的socket連接,發起read系統調用,用戶執行緒阻塞。內核開始複製數據,將數據從內核緩衝區複製到用戶緩衝區。

(4)複製完成後,內核返回結果,用戶執行緒才會解除阻塞的狀態,用戶執行緒讀取到了數據,繼續執行。

IO多路復用模型的特點:IO多路復用模型的IO涉及兩種系統調用(System Call),另一種是select/epoll(就緒查詢),一種是IO操作。IO多路復用模型建立在作業系統的基礎設施之上,即作業系統的內核必須能夠提供多路分離的系統調用select/epoll。

和NIO模型相似,多路復用IO也需要輪詢。負責select/epoll狀態查詢調用的執行緒,需要不斷地進行select/epoll輪詢,查找出達到IO操作就緒的socket連接。 IO多路復用模型與同步非阻塞IO模型是有密切關係的。對於註冊在選擇器上的每一個可以查詢的socket連接,一般都設置成為同步非阻塞模型。僅是這一點,對於用戶程式而言是無感知的。 IO多路復用模型的優點:與一個執行緒維護一個連接的阻塞IO模式相比,使用select/epoll的最大優勢在於,一個選擇器查詢執行緒可以同時處理成千上萬個連接(Connection)。系統不必創建大量的執行緒,也不必維護這些執行緒,從而大大減小了系統的開銷。

Java語言的NIO(New IO)技術,使用的就是IO多路復用模型。在Linux系統上,使用的是epoll系統調用。 IO多路復用模型的缺點:本質上,select/epoll系統調用是阻塞式的,屬於同步IO。都需要在讀寫事件就緒後,由系統調用本身負責進行讀寫,也就是說這個讀寫過程是阻塞的。 如何徹底地解除執行緒的阻塞,就必須使用非同步IO模型。

非同步IO(Asynchronous IO)

非同步IO,指的是用戶空間與內核空間的調用方式反過來。用戶空間的執行緒變成被動接受者,而內核空間成了主動調用者。這有點類似於Java中比較典型的回調模式,用戶空間的執行緒向內核空間註冊了各種IO事件的回調函數,由內核去主動調用。

舉個例子。發起一個非同步IO的read讀操作的系統調用,流程如下: (1)當用戶執行緒發起了read系統調用,立刻就可以開始去做其他的事,用戶執行緒不阻塞。 (2)內核就開始了IO的第一個階段:準備數據。等到數據準備好了,內核就會將數據從內核緩衝區複製到用戶緩衝區(用戶空間的記憶體)。 (3)內核會給用戶執行緒發送一個訊號(Signal),或者回調用戶執行緒註冊的回調介面,告訴用戶執行緒read操作完成了。 (4)用戶執行緒讀取用戶緩衝區的數據,完成後續的業務操作。

非同步IO模型的特點:在內核等待數據和複製數據的兩個階段,用戶執行緒都不是阻塞的。用戶執行緒需要接收內核的IO操作完成的事件,或者用戶執行緒需要註冊一個IO操作完成的回調函數。正因為如此,非同步IO有的時候也被稱為訊號驅動IO。

非同步IO非同步模型的缺點:應用程式僅需要進行事件的註冊與接收,其餘的工作都留給了作業系統,也就是說,需要底層內核提供支援。

理論上來說,非同步IO是真正的非同步輸入輸出,它的吞吐量高於IO多路復用模型的吞吐量。 就目前而言,Windows系統下通過IOCP實現了真正的非同步IO。而在Linux系統下,非同步IO模型在2.6版本才引入,目前並不完善,其底層實現仍使用epoll,與IO多路復用相同,因此在性能上沒有明顯的優勢。 大多數的高並發伺服器端的程式,一般都是基於Linux系統的。因而,目前這類高並發網路應用程式的開發,大多採用IO多路復用模型。 大名鼎鼎的Netty框架,使用的就是IO多路復用模型,而不是非同步IO模型。

整理自《Netty、Redis、Zookeeper高並發實戰》