1. 程式人生 > >詳解 Java 中 4 種 I/O 模型

詳解 Java 中 4 種 I/O 模型

同步、非同步、阻塞、非阻塞都是和I/O(輸入輸出)有關的概念,最簡單的檔案讀取就是I/O操作。而在檔案讀取這件事兒上,可以有多種方式。

本篇會先介紹一下I/O的基本概念,通過一個生活例子來分別解釋下這幾種I/O模型,以及Java支援的I/O模型。

基本概念

在解釋I/O模型之前,我先說明一下幾個作業系統的概念

檔案描述符fd

檔案描述符(file descriptor)是電腦科學中的一個術語,是一個用於表述指向檔案的引用的抽象化概念。

檔案描述符在形式上是一個非負整數。實際上,它是一個索引值,指向核心為每一個程序所維護的該程序開啟檔案的記錄表。 當程式開啟一個現有檔案或者建立一個新檔案時,核心向程序返回一個檔案描述符。 在程式設計中,一些涉及底層的程式編寫往往會圍繞著檔案描述符展開。檔案描述符這一概念往往只適用於UNIX、Linux這樣的作業系統。

快取I/O

快取I/O又被稱作標準I/O,大多數檔案系統的預設I/O操作都是快取I/O。在Linux的快取I/O機制中, 作業系統會將I/O的資料快取在檔案系統的頁快取中,也就是說,資料會先被拷貝到作業系統核心的緩衝區中, 然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。

快取I/O的缺點是資料在傳輸過程中需要在應用程式地址空間和核心進行多次資料拷貝操作,這些資料拷貝操作所帶來的CPU以及記憶體開銷是非常大的。

下面我以一個生活中燒開水的例子來形象解釋一下同步、非同步、阻塞、非阻塞概念。

同步和非同步

說到燒水,我們都是通過熱水壺來燒水的。在很久之前,科技還沒有這麼發達的時候,如果我們要燒水, 需要把水壺放到火爐上,我們通過觀察水壺內的水的沸騰程度來判斷水有沒有燒開。

隨著科技的發展,現在市面上的水壺都有了提醒功能,當我們把水壺插電之後,水壺水燒開之後會通過聲音提醒我們水開了。

對於燒水這件事兒來說,傳統水壺的燒水就是同步的,高科技水壺的燒水就是非同步的。

同步請求

A呼叫B,B的處理是同步的,在處理完之前他不會通知A,只有處理完之後才會明確的通知A。

非同步請求

A呼叫B,B的處理是非同步的,B在接到請求後先告訴A我已經接到請求了,然後非同步去處理,處理完之後通過回撥等方式再通知A。

所以說,同步和非同步最大的區別就是被呼叫方的執行方式和返回時機。 同步指的是被呼叫方做完事情之後再返回,非同步指的是被呼叫方先返回,然後再做事情,做完之後再想辦法通知呼叫方。

阻塞和非阻塞

還是那個燒水的例子,當你把水放到水壺裡面,按下開關後,你可以坐在水壺前面,別的事情什麼都不做, 一直等著水燒好。你還可以先去客廳看電視,等著水開就好了。

對於你來說,坐在水壺前面等就是阻塞的,去客廳看電視等著水開就是非阻塞的。

阻塞請求

A呼叫B,A一直等著B的返回,別的事情什麼也不幹。

非阻塞請求

A呼叫B,A不用一直等著B的返回,先去忙別的事情了。

所以說,阻塞和非阻塞最大的區別就是在被呼叫方返回結果之前的這段時間內,呼叫方是否一直等待。 阻塞指的是呼叫方一直等待別的事情什麼都不做。非阻塞指的是呼叫方先去忙別的事情。

阻塞、非阻塞和同步、非同步的區別

首先,前面已經提到過,阻塞、非阻塞和同步、非同步其實針對的物件是不一樣的。

給我大聲念三遍下面的句子

阻塞、非阻塞說的是呼叫者。同步、非同步說的是被呼叫者。

阻塞、非阻塞說的是呼叫者。同步、非同步說的是被呼叫者。

阻塞、非阻塞說的是呼叫者。同步、非同步說的是被呼叫者。

有人認為阻塞和同步是一回事兒,非阻塞和非同步是一回事。但是這是不對的。

同步包含阻塞和非阻塞

我們是用傳統的水壺燒水。在水燒開之前我們一直做在水壺前面,等著水開。這就是阻塞的。

我們是用傳統的水壺燒水。在水燒開之前我們先去客廳看電視了,但是水壺不會主動通知我們, 需要我們時不時的去廚房看一下水有沒有燒開,這就是非阻塞的。

非同步包含阻塞和非阻塞

我們是用帶有提醒功能的水壺燒水。在水燒發出提醒之前我們一直做在水壺前面,等著水開。這就是阻塞的。

我們是用帶有提醒功能的水壺燒水。在水燒發出提醒之前我們先去客廳看電視了,等水壺發出聲音提醒我們。這就是非阻塞的。

Unix中的五種I/O模型

對於一次I/O訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。 所以說,當一個read操作發生時,它會經歷兩個階段:

第一階段:等待資料準備 (Waiting for the data to be ready)。

第二階段:將資料從核心拷貝到程序中 (Copying the data from the kernel to the process)。

 

對於socket流而言

第一階段:通常涉及等待網路上的資料分組到達,也就是被複制到核心的某個緩衝區。

第二階段:把資料從核心緩衝區複製到應用程序緩衝區。

 

Unix下五種I/O模型:

  1. 同步阻塞I/O

  2. 同步非阻塞I/O

  3. I/O多路複用(select和poll)

  4. 訊號驅動I/O(SIGIO)

  5. 非同步非阻塞 IO

同步阻塞I/O

阻塞I/O下請求無法立即完成則保持阻塞,阻塞I/O分為如下兩個階段。

階段1:等待資料就緒。網路I/O的情況就是等待遠端資料陸續抵達,也就是網路資料被複制到核心快取區中,磁碟I/O的情況就是等待磁碟資料從磁碟上讀取到核心態記憶體中。

階段2:資料拷貝。出於系統安全,使用者態的程式沒有許可權直接讀取核心態記憶體,因此核心負責把核心態記憶體中的資料拷貝一份到使用者態記憶體中。

這兩個階段必須都完成後才能繼續下一步操作

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

同步非阻塞I/O

就是階段1的時候使用者程序可選擇做其他事情,通過輪詢的方式看看核心緩衝區是否就緒。如果資料就緒,再去執行階段2。

也就是說非阻塞的recvform系統呼叫呼叫之後,程序並沒有被阻塞,核心馬上返回給程序,如果資料還沒準備好, 此時會返回一個error。程序在返回之後,可以乾點別的事情,然後再發起recvform系統呼叫。

重複上面的過程, 迴圈往復的進行recvform系統呼叫。這個過程通常被稱之為輪詢。輪詢檢查核心資料,直到資料準備好, 再拷貝資料到程序,進行資料處理。需要注意,第2階段的拷貝資料整個過程,程序仍然是屬於阻塞的狀態。

在linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程如圖所示:

所以,nonblocking IO的特點是使用者程序需要不斷的主動詢問kernel資料好了沒有。

I/O多路複用

我這裡只想重點解釋一下I/O多路複用這種模型,因為現在用的最多。很多地方也稱為事件驅動IO模型,只是叫法不同,意思都一個樣。

IO多路複用是指核心一旦發現程序指定的一個或者多個IO條件準備讀取,它就通知該程序。

目前支援I/O多路複用的系統呼叫有 select、pselect、poll、epoll,I/O多路複用就是通過一種機制,一個程序可以監視多個描述符, 一旦某個檔案描述符fd就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。 

但select、pselect、poll、epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的, 而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

相比較於同步非阻塞I/O,它的改進的地方在於,原來需要使用者程序去輪詢的這事兒交給了核心執行緒幫你完成, 而且這個核心執行緒可以等待多個socket,能實現同時對多個IO埠進行監聽。

多路複用的特點是通過一種機制一個程序能同時等待IO檔案描述符,核心監視這些檔案描述符(套接字描述符), 其中的任意一個進入讀就緒狀態,select, poll,epoll函式就可以返回。對於監視的方式, 又可以分為 select, poll, epoll三種方式。

所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用多執行緒 + 阻塞IO的web server效能更好,可能延遲還更大。 也就是說,select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。

高併發的程式一般使用同步非阻塞方式而非多執行緒 + 同步阻塞方式。要理解這一點,首先要扯到併發和並行的區別。 比如去某部門辦事需要依次去幾個視窗,辦事大廳裡的人數就是併發數,而視窗個數就是並行度。 也就是說併發數是指同時進行的任務數(如同時服務的 HTTP 請求),而並行數是可以同時工作的物理資源數量(如 CPU 核數)。 

通過合理排程任務的不同階段,併發數可以遠遠大於並行度,這就是區區幾個 CPU 可以支援上萬個使用者併發請求的奧祕。 在這種高併發的情況下,為每個任務(使用者請求)建立一個程序或執行緒的開銷非常大。而同步非阻塞方式可以把多個 IO 請求丟到後臺去, 這就可以在一個程序裡服務大量的併發 IO 請求。

IO多路複用歸為同步阻塞模式

非同步非阻塞 IO

相對於同步IO,非同步IO不是順序執行。使用者程序進行aio_read系統呼叫之後,無論核心資料是否準備好,都會直接返回給使用者程序, 然後使用者態程序可以去做別的事情。等到socket資料準備好了,核心直接複製資料給程序,然後從核心向程序傳送通知。IO兩個階段, 程序都是非阻塞的。

Linux提供了AIO庫函式實現非同步,但是用的很少。目前有很多開源的非同步IO庫,例如libevent、libev、libuv。非同步過程如下圖所示:

更詳細的分析可參考 聊聊Linux5種IO模型

Java中四種I/O模型

上一章所述Unix中的五種I/O模型,除訊號驅動I/O外,Java對其它四種I/O模型都有所支援。

  1. Java傳統IO模型即是同步阻塞I/O

  2. NIO是同步非阻塞I/O

  3. 通過NIO實現的Reactor模式即是I/O多路複用模型的實現

  4. 通過AIO實現的Proactor模式即是非同步I/O模型的實現