1. 程式人生 > >從底層入手,圖解 Java NIO BIO MIO AIO 四大IO模型與原理

從底層入手,圖解 Java NIO BIO MIO AIO 四大IO模型與原理

目錄

瘋狂創客圈 Java 分散式聊天室【 億級流量】實戰系列之 -21【

部落格園 總入口


寫在前面

​ 大家好,我是作者尼恩。

​ 很多的小夥伴,被java IO 模型中,搞得有點兒暈,一會兒是4種模型,一會兒又變成了5種模型。

​ 很多的小夥伴,也被nio這個名詞搞暈了,一會兒java 的nio 不叫 非阻塞io,一會兒java nio 又是非阻塞io,到底是啥呢?

​ 很多的小夥伴,被非同步和非阻塞搞暈了。都非阻塞了,難道不是非同步的嗎?

​ 這這,好難呀。

​ 此文,從底層入手,給各位小夥伴,起底一下,java的四大io模型。需要面試的,或者沒有弄清楚的小夥伴,徹底的有福了。

​ 順便說下:今天是【瘋狂創客圈】的百萬級流量 Netty 聊天器 打造的系列文章的第21篇,這是一個非常重要的基礎篇。

1.1. Java IO讀寫原理

無論是Socket的讀寫還是檔案的讀寫,在Java層面的應用開發或者是linux系統底層開發,都屬於輸入input和輸出output的處理,簡稱為IO讀寫。在原理上和處理流程上,都是一致的。區別在於引數的不同。

使用者程式進行IO的讀寫,基本上會用到read&write兩大系統呼叫。可能不同作業系統,名稱不完全一樣,但是功能是一樣的。

先強調一個基礎知識:read系統呼叫,並不是把資料直接從物理裝置,讀資料到記憶體。write系統呼叫,也不是直接把資料,寫入到物理裝置。

read系統呼叫,是把資料從核心緩衝區複製到程序緩衝區;而write系統呼叫,是把資料從程序緩衝區複製到核心緩衝區。這個兩個系統呼叫,都不負責資料在核心緩衝區和磁碟之間的交換。底層的讀寫交換,是由作業系統kernel核心完成的。

1.1.1. 核心緩衝與程序緩衝區

緩衝區的目的,是為了減少頻繁的系統IO呼叫。大家都知道,系統呼叫需要儲存之前的程序資料和狀態等資訊,而結束呼叫之後回來還需要恢復之前的資訊,為了減少這種損耗時間、也損耗效能的系統呼叫,於是出現了緩衝區。

有了緩衝區,作業系統使用read函式把資料從核心緩衝區複製到程序緩衝區,write把資料從程序緩衝區複製到核心緩衝區中。等待緩衝區達到一定數量的時候,再進行IO的呼叫,提升效能。至於什麼時候讀取和儲存則由核心來決定,使用者程式不需要關心。

在linux系統中,系統核心也有個緩衝區叫做核心緩衝區。每個程序有自己獨立的緩衝區,叫做程序緩衝區。

所以,使用者程式的IO讀寫程式,大多數情況下,並沒有進行實際的IO操作,而是在讀寫自己的程序緩衝區。

1.1.2. java IO讀寫的底層流程

使用者程式進行IO的讀寫,基本上會用到系統呼叫read&write,read把資料從核心緩衝區複製到程序緩衝區,write把資料從程序緩衝區複製到核心緩衝區,它們不等價於資料在核心緩衝區和磁碟之間的交換。

在這裡插入圖片描述

首先看看一個典型Java 服務端處理網路請求的典型過程:

(1)客戶端請求

Linux通過網絡卡,讀取客戶斷的請求資料,將資料讀取到核心緩衝區。

(2)獲取請求資料

伺服器從核心緩衝區讀取資料到Java程序緩衝區。

(1)伺服器端業務處理

Java服務端在自己的使用者空間中,處理客戶端的請求。

(2)伺服器端返回資料

Java服務端已構建好的響應,從使用者緩衝區寫入系統緩衝區。

(3)傳送給客戶端

Linux核心通過網路 I/O ,將核心緩衝區中的資料,寫入網絡卡,網絡卡通過底層的通訊協議,會將資料傳送給目標客戶端。

1.2. 四種主要的IO模型

伺服器端程式設計經常需要構造高效能的IO模型,常見的IO模型有四種:

(1)同步阻塞IO(Blocking IO)

首先,解釋一下這裡的阻塞與非阻塞:

阻塞IO,指的是需要核心IO操作徹底完成後,才返回到使用者空間,執行使用者的操作。阻塞指的是使用者空間程式的執行狀態,使用者空間程式需等到IO操作徹底完成。傳統的IO模型都是同步阻塞IO。在java中,預設建立的socket都是阻塞的。

其次,解釋一下同步與非同步:

同步IO,是一種使用者空間與核心空間的呼叫發起方式。同步IO是指使用者空間執行緒是主動發起IO請求的一方,核心空間是被動接受方。非同步IO則反過來,是指核心kernel是主動發起IO請求的一方,使用者執行緒是被動接受方。

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

非阻塞IO,指的是使用者程式不需要等待核心IO操作完成後,核心立即返回給使用者一個狀態值,使用者空間無需等到核心的IO操作徹底完成,可以立即返回使用者空間,執行使用者的操作,處於非阻塞的狀態。

簡單的說:阻塞是指使用者空間(呼叫執行緒)一直在等待,而且別的事情什麼都不做;非阻塞是指使用者空間(呼叫執行緒)拿到狀態就返回,IO操作可以幹就幹,不可以幹,就去幹的事情。

非阻塞IO要求socket被設定為NONBLOCK。

強調一下,這裡所說的NIO(同步非阻塞IO)模型,並非Java的NIO(New IO)庫。

(3)IO多路複用(IO Multiplexing)

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

(5)非同步IO(Asynchronous IO)

非同步IO,指的是使用者空間與核心空間的呼叫方式反過來。使用者空間執行緒是變成被動接受的,核心空間是主動呼叫者。

這一點,有點類似於Java中比較典型的模式是回撥模式,使用者空間執行緒向核心空間註冊各種IO事件的回撥函式,由核心去主動呼叫。

1.3. 同步阻塞IO(Blocking IO)

在linux中的Java程序中,預設情況下所有的socket都是blocking IO。在阻塞式 I/O 模型中,應用程式在從IO系統呼叫開始,一直到到系統呼叫返回,這段時間是阻塞的。返回成功後,應用程序開始處理使用者空間的快取資料。

在這裡插入圖片描述

舉個栗子,發起一個blocking socket的read讀作業系統呼叫,流程大概是這樣:

(1)當用戶執行緒呼叫了read系統呼叫,核心(kernel)就開始了IO的第一個階段:準備資料。很多時候,資料在一開始還沒有到達(比如,還沒有收到一個完整的Socket資料包),這個時候kernel就要等待足夠的資料到來。

(2)當kernel一直等到資料準備好了,它就會將資料從kernel核心緩衝區,拷貝到使用者緩衝區(使用者記憶體),然後kernel返回結果。

(3)從開始IO讀的read系統呼叫開始,使用者執行緒就進入阻塞狀態。一直到kernel返回結果後,使用者執行緒才解除block的狀態,重新執行起來。

所以,blocking IO的特點就是在核心進行IO執行的兩個階段,使用者執行緒都被block了。

BIO的優點:

程式簡單,在阻塞等待資料期間,使用者執行緒掛起。使用者執行緒基本不會佔用 CPU 資源。

BIO的缺點:

一般情況下,會為每個連線配套一條獨立的執行緒,或者說一條執行緒維護一個連線成功的IO流的讀寫。在併發量小的情況下,這個沒有什麼問題。但是,當在高併發的場景下,需要大量的執行緒來維護大量的網路連線,記憶體、執行緒切換開銷會非常巨大。因此,基本上,BIO模型在高併發場景下是不可用的。

1.4. 同步非阻塞NIO(None Blocking IO)

在linux系統下,可以通過設定socket使其變為non-blocking。NIO 模型中應用程式在一旦開始IO系統呼叫,會出現以下兩種情況:

(1)在核心緩衝區沒有資料的情況下,系統呼叫會立即返回,返回一個呼叫失敗的資訊。

(2)在核心緩衝區有資料的情況下,是阻塞的,直到資料從核心緩衝複製到使用者程序緩衝。複製完成後,系統呼叫返回成功,應用程序開始處理使用者空間的快取資料。
在這裡插入圖片描述

舉個栗子。發起一個non-blocking socket的read讀作業系統呼叫,流程是這個樣子:

(1)在核心資料沒有準備好的階段,使用者執行緒發起IO請求時,立即返回。使用者執行緒需要不斷地發起IO系統呼叫。

(2)核心資料到達後,使用者執行緒發起系統呼叫,使用者執行緒阻塞。核心開始複製資料。它就會將資料從kernel核心緩衝區,拷貝到使用者緩衝區(使用者記憶體),然後kernel返回結果。

(3)使用者執行緒才解除block的狀態,重新執行起來。經過多次的嘗試,使用者執行緒終於真正讀取到資料,繼續執行。

NIO的特點:

應用程式的執行緒需要不斷的進行 I/O 系統呼叫,輪詢資料是否已經準備好,如果沒有準備好,繼續輪詢,直到完成系統呼叫為止。

NIO的優點:每次發起的 IO 系統呼叫,在核心的等待資料過程中可以立即返回。使用者執行緒不會阻塞,實時性較好。

NIO的缺點:需要不斷的重複發起IO系統呼叫,這種不斷的輪詢,將會不斷地詢問核心,這將佔用大量的 CPU 時間,系統資源利用率較低。

總之,NIO模型在高併發場景下,也是不可用的。一般 Web 伺服器不使用這種 IO 模型。一般很少直接使用這種模型,而是在其他IO模型中使用非阻塞IO這一特性。java的實際開發中,也不會涉及這種IO模型。

再次說明,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一種模型,叫做IO多路複用模型( IO multiplexing )。

1.5. IO多路複用模型(I/O multiplexing)

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

IO多路複用模型,就是通過一種新的系統呼叫,一個程序可以監視多個檔案描述符,一旦某個描述符就緒(一般是核心緩衝區可讀/可寫),核心kernel能夠通知程式進行相應的IO系統呼叫。

目前支援IO多路複用的系統呼叫,有 select,epoll等等。select系統呼叫,是目前幾乎在所有的作業系統上都有支援,具有良好跨平臺特性。epoll是在linux 2.6核心中提出的,是select系統呼叫的linux增強版本。

IO多路複用模型的基本原理就是select/epoll系統呼叫,單個執行緒不斷的輪詢select/epoll系統呼叫所負責的成百上千的socket連線,當某個或者某些socket網路連線有資料到達了,就返回這些可以讀寫的連線。因此,好處也就顯而易見了——通過一次select/epoll系統呼叫,就查詢到到可以讀寫的一個甚至是成百上千的網路連線。

舉個栗子。發起一個多路複用IO的的read讀作業系統呼叫,流程是這個樣子:

在這裡插入圖片描述

在這種模式中,首先不是進行read系統調動,而是進行select/epoll系統呼叫。當然,這裡有一個前提,需要將目標網路連線,提前註冊到select/epoll的可查詢socket列表中。然後,才可以開啟整個的IO多路複用模型的讀流程。

(1)進行select/epoll系統呼叫,查詢可以讀的連線。kernel會查詢所有select的可查詢socket列表,當任何一個socket中的資料準備好了,select就會返回。

當用戶程序呼叫了select,那麼整個執行緒會被block(阻塞掉)。

(2)使用者執行緒獲得了目標連線後,發起read系統呼叫,使用者執行緒阻塞。核心開始複製資料。它就會將資料從kernel核心緩衝區,拷貝到使用者緩衝區(使用者記憶體),然後kernel返回結果。

(3)使用者執行緒才解除block的狀態,使用者執行緒終於真正讀取到資料,繼續執行。

多路複用IO的特點:

IO多路複用模型,建立在作業系統kernel核心能夠提供的多路分離系統呼叫select/epoll基礎之上的。多路複用IO需要用到兩個系統呼叫(system call), 一個select/epoll查詢呼叫,一個是IO的讀取呼叫。

和NIO模型相似,多路複用IO需要輪詢。負責select/epoll查詢呼叫的執行緒,需要不斷的進行select/epoll輪詢,查找出可以進行IO操作的連線。

另外,多路複用IO模型與前面的NIO模型,是有關係的。對於每一個可以查詢的socket,一般都設定成為non-blocking模型。只是這一點,對於使用者程式是透明的(不感知)。

多路複用IO的優點:

用select/epoll的優勢在於,它可以同時處理成千上萬個連線(connection)。與一條執行緒維護一個連線相比,I/O多路複用技術的最大優勢是:系統不必建立執行緒,也不必維護這些執行緒,從而大大減小了系統的開銷。

Java的NIO(new IO)技術,使用的就是IO多路複用模型。在linux系統上,使用的是epoll系統呼叫。

多路複用IO的缺點:

本質上,select/epoll系統呼叫,屬於同步IO,也是阻塞IO。都需要在讀寫事件就緒後,自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。

如何充分的解除執行緒的阻塞呢?那就是非同步IO模型。

1.6. 非同步IO模型(asynchronous IO)

如何進一步提升效率,解除最後一點阻塞呢?這就是非同步IO模型,全稱asynchronous I/O,簡稱為AIO。

AIO的基本流程是:使用者執行緒通過系統呼叫,告知kernel核心啟動某個IO操作,使用者執行緒返回。kernel核心在整個IO操作(包括資料準備、資料複製)完成後,通知使用者程式,使用者執行後續的業務操作。

kernel的資料準備是將資料從網路物理裝置(網絡卡)讀取到核心緩衝區;kernel的資料複製是將資料從核心緩衝區拷貝到使用者程式空間的緩衝區。

在這裡插入圖片描述

(1)當用戶執行緒呼叫了read系統呼叫,立刻就可以開始去做其它的事,使用者執行緒不阻塞。

(2)核心(kernel)就開始了IO的第一個階段:準備資料。當kernel一直等到資料準備好了,它就會將資料從kernel核心緩衝區,拷貝到使用者緩衝區(使用者記憶體)。

(3)kernel會給使用者執行緒傳送一個訊號(signal),或者回呼叫戶執行緒註冊的回撥介面,告訴使用者執行緒read操作完成了。

(4)使用者執行緒讀取使用者緩衝區的資料,完成後續的業務操作。

非同步IO模型的特點:

在核心kernel的等待資料和複製資料的兩個階段,使用者執行緒都不是block(阻塞)的。使用者執行緒需要接受kernel的IO操作完成的事件,或者說註冊IO操作完成的回撥函式,到作業系統的核心。所以說,非同步IO有的時候,也叫做訊號驅動 IO 。

非同步IO模型缺點:

需要完成事件的註冊與傳遞,這裡邊需要底層作業系統提供大量的支援,去做大量的工作。

目前來說, Windows 系統下通過 IOCP 實現了真正的非同步 I/O。但是,就目前的業界形式來說,Windows 系統,很少作為百萬級以上或者說高併發應用的伺服器作業系統來使用。

而在 Linux 系統下,非同步IO模型在2.6版本才引入,目前並不完善。所以,這也是在 Linux 下,實現高併發網路程式設計時都是以 IO 複用模型模式為主。

小結一下:

四種IO模型,理論上越往後,阻塞越少,效率也是最優。在這四種 I/O 模型中,前三種屬於同步 I/O,因為其中真正的 I/O 操作將阻塞執行緒。只有最後一種,才是真正的非同步 I/O 模型,可惜目前Linux 作業系統尚欠完善。

寫在最後

​ 本篇作為[瘋狂創客圈]的百萬級流量高併發實戰的一篇基礎篇的文章,後續還有分散式協調,分散式閘道器等系列文章出來。


瘋狂創客圈 百萬級流量 高併發實戰

  • Java (Netty) 聊天程式【 億級流量】實戰 開源專案實戰

  • Netty 原始碼、原理、JAVA NIO 原理
  • Java 面試題 一網打盡
  • 瘋狂創客圈 【 部落格園 總入口 】