BIO、NIO、AIO

ens發表於2020-04-05

一、Linux 基礎知識回顧

1. 使用者空間和核心空間

現在作業系統都採用虛擬定址,處理器先產生一個虛擬地址,通過地址翻譯成實體地址(記憶體的地址),再通過匯流排的傳遞,最後處理器拿到某個實體地址返回的位元組。

對32位作業系統而言,它的定址空間(虛擬儲存空間)為4G(2的32次方)。作業系統的核心是核心,獨立於普通的應用程式,可以訪問受保護的記憶體空間,也有訪問底層硬體裝置的所有許可權。
為了保證使用者程式不能直接操作核心(kernel),保證核心的安全,操心繫統將虛擬空間劃分為兩部分,一部分為核心空間,一部分為使用者空間。
針對linux作業系統而言:
將最高的1G位元組(從虛擬地址0xC0000000到0xFFFFFFFF),供核心使用,稱為核心空間。
而將較低的3G位元組(從虛擬地址0x00000000到0xBFFFFFFF),供各個程式使用,稱為使用者空間。

2. 直接I/O和快取I/O

檔案系統IO 分為 DirectIO(直接I/O)和 BufferIO(快取 I/O),其中 BufferIO 也叫Normal IO(標準 I/O)。大多數檔案系統的預設 I/O 操作都是快取 I/O。

快取 I/O

讀操作:作業系統檢查核心的緩衝區有沒有需要的資料,如果已經快取了,那麼就直接從快取中返回;否則從磁碟中讀取,然後快取在作業系統的快取中。
寫操作:將資料從使用者空間複製到核心空間的快取中。這時對使用者程式來說寫操作就已經完成,至於什麼時候再寫到磁碟中由作業系統決定,除非顯示地呼叫了sync同步命令。
write 為例,資料會先被拷貝程式緩衝區,在拷貝到作業系統核心的緩衝區中,然後才會寫到儲存裝置中。

BIO、NIO、AIO

直接 I/O(少了拷貝到應用程式緩衝區這一步)

BIO、NIO、AIO

3.阻塞與同步

1 ) 阻塞(Block) / 非租塞(NonBlock)
阻塞和非阻塞是程式在訪問資料的時候,資料是否準備就緒的一種處理方式,比如當資料沒有準備就緒的時候
阻塞:往往需要等待緩衝區中的資料準備好過後才處理其他的事情,否則一直等待在那裡。
非阻塞:當我們的程式訪問我們的資料緩衝區的時候,如果資料沒有準備好則直接返回,不會等待。如果資料已經準備好,也直接返回。

2 ) 同步(Synchronization) / 非同步(Asynchronization)
同步和非同步都是基於應用程式私作業系統處理IO事件所採用的方式,比如
同步:是應用程式要直接參與IO讀寫的操作。
非同步:所有的IO讀寫交給作業系統去處理,應用程式只需要等待通知。
同步方式在處理IO事件的時候,必須阻塞在某個方法上面等待我們的IO事件完成(阻塞IO事件或者通過輪詢IO事件的方式)。
對於非同步來說,所有的IO讀寫都交給了作業系統。這個時候,我們可以去做其他的事情,並不需要去完成真正的IO操作,當操作完成IO後.會給我們的應用程式一個通知。

二、常見 IO 模型

對於一次IO訪問,它會經歷兩個階段:

  1. 等待資料準備就緒 (Waiting for the data to be ready)
  2. 操作:將資料從核心拷貝到程式中 (Copying the data from the kernel to the process)

舉例來說:
讀函式:分為等待系統可讀和真正的讀。
寫函式:分為等待網路卡可以寫和真正的寫。
說明
等待就緒的阻塞是不使用 CPU 的,是在“空等”。
而真正的讀寫操作的阻塞是使用 CPU 的,真正在“幹活”,而且這個過程非常快,屬於memory copy,寬頻通常在 1GB/s 級別以上,可以理解為基本不耗時。

下圖是幾種常見I/O模型的對比

BIO、NIO、AIO
以socket.read()為例子:

傳統的BIO裡面socket.read(),如果TCP RecvBuffer裡沒有資料,函式會一直阻塞,直到收到資料,返回讀到的資料。
對於NIO,如果TCP RecvBuffer有資料,就把資料從網路卡讀到記憶體,並且返回給使用者;反之則直接返回0,永遠不會阻塞。
最新的AIO(Async I/O)裡面會更進一步:不但等待就緒是非阻塞的,就連資料從網路卡到記憶體的過程也是非同步的。
換句話說,BIO裡使用者最關心“我要讀”,NIO裡使用者最關心"我可以讀了",在AIO模型裡使用者更需要關注的是“讀完了”。
NIO一個重要的特點是:socket主要的讀、寫、註冊和接收函式,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但效能非常高)。

三、什麼是 BIO、NIO、AIO

1. 同步阻塞I/O(BIO)

同步阻塞I/O,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,可以通過執行緒池機制來改善。
BIO方式適用於連線數目比較小且固定的架構,這種方式對服務端資源要求比較高,併發侷限於應用中,在jdk1.4以前是唯一的io選擇,但程式直觀簡單易理解。

BIO圖解

BIO、NIO、AIO
偽非同步模型IO
也被成為M:N客戶服務模型。即通過執行緒池模型的形式用M個執行緒來服務N個客戶端的連線;其中M的大小可以根據伺服器的配置來設定最大值,而可服務客戶端個數N則可以遠遠的大於M.這樣來提高伺服器的服務效率,提高執行緒利用率。同BIO模型類似,只不過,Acceptor接受客戶端請求後,不再獨立啟動執行緒來處理,而是將客戶請求交給執行緒池來處理,從而減少執行緒的建立數量,提高執行緒利用率,增加伺服器的處理能力;
偽非同步IO圖解
BIO、NIO、AIO

2. 同步非阻塞I/O(NIO)

同步非阻塞I/O,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有IO請求時才啟動一個執行緒進行處理。NIO方式適用於連線數目多且連線比較短(輕操作)的架構,比如聊天伺服器,併發侷限於應用中,程式設計比較複雜,jdk1.4開始支援。

I/O多路複用模型

I/O多路複用:I/O就是指的我們網路I/O,多路指多個TCP連線(或多個Channel),複用指複用一個或少量執行緒。串起來理解就是很多個網路I/O複用一個或少量的執行緒來處理這些連線。

多路複用的優勢並不是單個連線處理的更快,而是在於能處理更多的連線。

BIO、NIO、AIO

目前的常用的IO複用模型有三種:select,poll,epoll。

I/O多路複用就是通過一種機制,一個程式可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

jdk1.4 是使用的 select/poll 模型
jdk1.5 以後把 select/poll 改為了epoll模型

1) select 模型

各個客戶端連線的檔案描述符(fd)也就是套接字,都被放到了一個集合中,呼叫select函式之後會一直監視這些檔案描述符中有哪些可讀,如果有可讀的描述符那麼我們的工作程式就去讀取資源。

我們在select函式中告訴核心需要監聽的不同狀態的檔案描述符以及能接受的超時時間,函式會返回所有狀態下就緒的描述符的個數,並且可以通過遍歷fdset,來找到就緒的檔案描述符。

存在的問題:

  • 每次呼叫select,都需要把待監控的fd集合從使用者態拷貝到核心態,當fd很大時,開銷很大。
  • 每次呼叫select,都需要輪詢一遍所有的fd,檢視就緒狀態。這個開銷在fd很多時也很大。
  • select支援的最大檔案描述符數量有限,預設是1024

2) poll 模型

相對 於select,poll 已不存在最大檔案描述符限制。

3) epoll 模型

epoll在Linux2.6核心正式提出,是基於事件驅動的I/O方式,相對於select來說,epoll沒有描述符個數限制,使用一個檔案描述符管理多個描述符,將使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次(可以理解為一塊公共記憶體,該記憶體既不屬於使用者態也不屬於核心態)。

select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。

epoll 的好處:

  • 避免記憶體級拷貝
  • 事件驅動(不是輪詢)

但是也並不是所有情況下 epoll 都比 select/poll 好,比如在如下場景:
在大多數客戶端都很活躍的情況下,系統會把所有的回撥函式都喚醒,所以會導致負載較高。既然要處理這麼多的連線,那倒不如 select/poll 遍歷簡單有效。

4)select & poll & epoll 比較

select poll epoll
操作方式 遍歷 遍歷 回撥
底層實現 陣列 連結串列 雜湊表
IO 效率 每次呼叫都進行線性遍歷,時間複雜度為O(n) 每次呼叫都進行線性遍歷,時間複雜度為O(n) 事件通知方式,每當fd就緒,系統註冊的回撥函式就會被呼叫,將就緒fd放到readyList裡面,時間複雜度O(1)
最大連線數 1024 無上限 無上限
fd 拷貝 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態 每次呼叫poll,都需要把fd集合從使用者態拷貝到核心態 呼叫epoll_ctl時拷貝進核心並儲存,之後每次epoll_wait不拷貝

NIO的3個核心概念

1) 緩衝區Buffer

Buffer是一個物件。它包含一些要寫入或者讀出的資料。在面向流的I/O中,可以將資料寫入或者將資料直接讀到Stream物件中。

在NIO中,所有的資料都是用緩衝區處理。IO是面向流的,NIO是面向緩衝區的。

最常用的緩衝區是ByteBuffer,一個ByteBuffer提供了一組功能用於操作byte陣列。除了ByteBuffer,還有其他的一些緩衝區,事實上,每一種Java基本型別(除了Boolean)都對應一種緩衝區,具體如下:

  • ByteBuffer:位元組緩衝區
  • CharBuffer:字元緩衝區
  • ShortBuffer:短整型緩衝區
  • IntBuffer:整型緩衝區
  • LongBuffer:長整型緩衝區
  • FloatBuffer:浮點型緩衝區
  • DoubleBuffer:雙精度浮點型緩衝區

2) 通道Channel

Channel是一個通道,可以通過它讀取和寫入資料,他就像自來水管一樣,網路資料通過Channel讀取和寫入。

通道和流不同之處在於通道是雙向的,流只是在一個方向移動,而且通道可以用於讀,寫或者同時用於讀寫。

因為Channel是全雙工的,所以它比流更好地對映底層作業系統的API,特別是在UNIX網路程式設計中,底層作業系統的通道都是全雙工的,同時支援讀和寫。

Channel有四種實現:

  • FileChannel:是從檔案中讀取資料。
  • DatagramChannel:從UDP網路中讀取或者寫入資料。
  • SocketChannel:從TCP網路中讀取或者寫入資料。
  • ServerSocketChannel:允許你監聽來自TCP的連線,就像伺服器一樣。每一個連線都會有一個SocketChannel產生。

3) 多路複用器Selector

Selector選擇器可以監聽多個Channel通道感興趣的事情(read、write、accept(服務端接收)、connect,實現一個執行緒管理多個Channel,節省執行緒切換上下文的資源消耗。Selector只能管理非阻塞的通道,FileChannel是阻塞的,無法管理。

關鍵物件
Selector:選擇器物件,通道註冊、通道監聽物件和Selector相關。
SelectorKey:通道監聽關鍵字,通過它來監聽通道狀態。

**監聽註冊 ** 監聽註冊在Selector

socketChannel.register(selector, SelectionKey.OP_READ);
複製程式碼

監聽的事件

  • OP_ACCEPT:接收就緒,serviceSocketChannel使用的
  • OP_READ:讀取就緒,socketChannel使用
  • OP_WRITE:寫入就緒,socketChannel使用
  • OP_CONNECT:連線就緒,socketChannel使用

NIO的應用和框架

1) NIO的應用

Java NIO成功的應用在了各種分散式、即時通訊和中介軟體Java系統中,充分的證明了基於NIO構建的通訊基礎,是一種高效,且擴充套件性很強的通訊架構。

例如:Dubbo(服務框架),就預設使用Netty作為基礎通訊元件,用於實現各程式節點之間的內部通訊。

Jetty、Mina、Netty、Dubbo、ZooKeeper等都是基於NIO方式實現。

Mina出身於開源界的大牛Apache組織 Netty出身於商業開源大亨Jboss Dubbo阿里分散式服務框架

2) NIO框架

特別是Netty是目前最流行的一個Java開源框架NIO框架,Netty提供非同步的、事件驅動的網路應用程式框架和工具,用以快速開發高效能、高可靠性的網路伺服器和客戶端程式。

相比JDK原生NIO,Netty提供了相對十分簡單易用的API,非常適合網路程式設計。

Mina和Netty這兩個NIO框架的創作者是同一個人Trustin Lee 。Netty從某種程度上講是Mina的延伸和擴充套件,解決了一些Mina上的設計缺陷,也優化了一下Mina上面的設計理念。

另一方面Netty相比較Mina的優勢:

  • 更容易學習
  • API更簡單
  • 詳細的範例原始碼和API文件
  • 更活躍的論壇和社群
  • 更高的程式碼更新維護速度

Netty無疑是NIO框架的首選,它的健壯性、功能、效能、可定製性和可擴充套件性在同類框架都是首屈一指的,後續將重點詳細談Netty的實現原理以及實戰場景。

3. 非同步非阻塞I/O(AIO)

伺服器實現模式為一個有效請求一個執行緒,客戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動執行緒進行處理。AIO方式適用於連線數目多且連線比較長(重操作)的架構,比如相簿伺服器,充分呼叫OS參與併發操作,程式設計比較複雜。

AIO又稱為NIO2,在JDK7才開始支援。

BIO、NIO、AIO
BIO、NIO、AIO

參考來源

IO模式和IO多路複用(阻塞IO、非阻塞IO、同步IO、非同步IO等概念)
BIO、NIO、AIO有什麼區別
圖解java的BI0,NIO,最簡單直白的理解同步和非同步IO模型
NIO原理詳解
詳解NIO與BIO的區別,NIO的執行原理及併發使用場景
【NIO】 I/O多路複用模型
IO多路複用的三種機制Select,Poll,Epoll
BIO、NIO、AIO、多路複用IO的區別(圖解)
IO、NIO、AIO 內部原理分析

相關文章