IO多路復用


要想學習netty就先要了解:(網絡編程模型:BIO、NIO、AIO)

IO

image
上圖的工作模式:

  • 開始時應用程序會發一個請求給CPU,CPU得到通知后,此時CPU就需要調用操作系統內核程序(磁盤控制器)。這就是用戶態->內核態
  • 磁盤控制器接到通知,使用DMA拷貝技術將數據放到PageCache內核緩沖區中,再由CPU把內核緩沖區中的數據傳回用戶緩沖區中(buffer)。這就是內核態->用戶態

請求數據時,IO操作通常包括兩個部分(使用到的IO模型如下)

  • 等待內核緩沖區中的數據准備好
  • 將內核緩沖區中的數據拷貝到用戶緩沖區中(需要使用零拷貝進行優化,詳情見零拷貝

一、IO模型

阻塞IO:程序請求操作系統IO,如果內核緩沖區中沒有准備好數據,進程則會進行等待。
非阻塞IO:程序請求操作系統IO,如果內核緩沖區中沒有准備好數據,進程會繼續執行,不斷進行系統調用直到IO數據准備好。
同步IO:操作系統收到程序請求后,如果內核緩沖區中沒有准備好數據,進程不會響應,直到數據准備好才會響應往下執行。
異步IO:操作系統收到程序請求后,如果內核緩沖區中沒有准備好數據,會返回一個標記,程序繼續往下執行。當數據准備好之后,會以事件的方式通知。

1.1同步IO

1、阻塞式IO(兩個步驟都需要等待)

應用進程被阻塞,直到內核緩沖區中的數據復制到應用進程緩沖區中才返回。
image

2、非阻塞式IO(第一個步驟不斷詢問數據是否准備好不需要等待,第二步需要等待)

應用進程執行系統調用之后,內核返回一個錯誤碼。應用進程可以繼續執行,但是需要不斷的執行系統調用來獲知I/O是否完成,這種方式稱為輪詢。(polling)。
image

3、I/O多路復用(兩個步驟都需要等待)

1、使用select或者poll等待數據,並且可以等待多個套接字中的任何一個變為可讀,這一過程會被阻塞,當某一個套接字可讀時返回,之后再使用recvfrom 把數據從內核復制到進程中。
2、使用select或者poll,可以讓單個線程具有處理多個I/O事件的能力
image

IO多路復用的實現(詳情查看大佬:IO多路復用

多路復用的實現:select、poll、epoll,這些函數都是系統調用函數。

select(一次select系統調用+N次就緒的客戶端的read系統調用)

select工作原理

  • 當通過一個線程調用select,將保存客戶端連接的數組拷貝一份給內核態,在內核層遍歷該數組看那個客戶端已經准備好數據,將准備好的客戶端的個數返回給用戶態,用戶態需要遍歷數組,找到准備好數據的客戶端,然后進行read系統調用將內核緩沖區中的數據拷貝到用戶緩沖區中。

select存在的問題

  1. select 調用需要傳入保存客戶端連接的數組,需要拷貝一份到內核,高並發場景下這樣的拷貝消耗的資源是驚人的。(可優化為不復制)
  2. select 在內核層仍然是通過遍歷的方式檢查客戶端的就緒狀態,是個同步過程,只不過無系統調用切換上下文的開銷,只在內核態遍歷。(內核層可優化為異步事件通知)
  3. select 僅僅返回就緒的客戶端的個數,具體哪個可讀還是要用戶自己遍歷。(可優化為只返回給用戶就緒的客戶端標識,無需用戶做無效的遍歷)
poll

它和select 的主要區別就是,去掉了select只能監聽1024個客戶端的限制。

epoll

epoll對select和poll進行了優化:

  1. 只在內核中存儲保存客戶端連接的數組,無需用戶每次都重新傳入,只需告訴內核修改的部分即可。
  2. 內核不再通過輪詢的方式找到就緒的客戶端,而是通過異步IO事件通知用戶態已經就緒的客戶端。
  3. 內核僅會將有IO事件的客戶端標識返回給用戶,用戶也無需遍歷整個文件描述符集合。

由於Linux下沒有Windows下的IOCP技術提供真正的異步IO支持,所以Linux下使用epoll模擬異步IO

4、信號驅動IO(第一步不會等待,第二步會等待)

1、應用進程使用sigaction系統調用,內核立即返回,應用進程可以繼續執行,也就是說等待數據階段應用進程是非阻塞的。
2、內核在數據到達時向應用進程發送SIGIO信號,應用進程收到之后在信號處理程序中調用 recvfrom 將數據從內核復制到應用進程中。
image

1.2異步IO(兩個步驟都不需要等待)

進行aio_read系統調用會立即返回,應用進程繼續執行,不會被阻塞,內核會在所有操作完成之后向應用進程發送信號。
image

二、IO的發展

image

2.1 BIO、NIO、AIO的區別

Java BIO同步並阻塞,服務器采用線程池為一個客戶端連接創建一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷。

  • 連接數目比較小且固定的架構,對服務器資源要求比較高

Java NIO同步非阻塞(IO多路復用),服務器實現模式為一個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求就進行處理。

  • 連接數目多且連接比較短(輕操作)的架構:聊天服務器,彈幕系統,服務器間通訊

Java AIO(NIO.2)異步非阻塞,AIO 引入異步通道的概念,采用了 Rector/Proactor 模式,當用戶態訪問內核會立即返回一個標記,由內核會在所有操作完成之后向應用進程發送信號,一般適用於連接數較多且連接時間較長的應用。

  • 連接數目多且連接比較長(重操作)的架構:相冊服務器

2.2 NIO的三個核心部分

  • BIO 以的方式處理數據,而 NIO 以的方式處理數據,塊 I/O 的效率比流 I/O 高很多。
  • BIO 是阻塞的,NIO 則是非阻塞的。
  • BIO 基於字節流和字符流進行操作,而 NIO 基於 Channel(通道)和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用於監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道。Buffer和Channel之間的數據流向是雙向的

channel

  • 通道可以同時進行讀寫,而流只能讀或者只能寫
  • 通道可以實現異步讀寫數據
  • 通道可以從緩沖讀數據,也可以寫數據到緩沖

channel類

  • FileChannel:用於文件的數據讀寫
  • DatagramChannel:用於 UDP 的數據讀寫
  • SocketChannel:用於 TCP 的數據讀寫
  • ServerSocketChannel:用於UDP和TCP數據的讀寫

channel類中幾個重要的方法

  • public int read(ByteBuffer dst):從通道讀取數據並放到緩沖區中(用於讀取文件中的數據)
  • public int write(ByteBuffer src):把緩沖區的數據寫到通道中(用於把數據寫到文件中)
  • public long transferFrom(ReadableByteChannel src, long position, long count):從目標通道中復制數據到當前通道
  • public long transferTo(long position, long count, WritableByteChannel target):把數據從當前通道復制給目標通道

buffer

緩沖區(Buffer):緩沖區本質上是一個可以讀寫數據的內存塊,可以理解成是一個容器對象(含數組),該對象提供了一組方法,可以更輕松地使用內存塊,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。對文件的讀取或寫入都必須經由Buffer

buffer的子類
image

Buffer緩沖區有兩種工作模式

  • 寫模式:當通道中的數據讀取到buffer中時,buffer為寫模式
  • 讀模式:當把buffer中的數據寫入到通道時,buffer為讀模式

buffer緩沖區中的四大屬性
需要使用buffer.flip反轉讀模式和寫模式
寫模式
image

讀模式
image

  • position: 緩存區分配下一個要被讀或寫的元素的索引,每次讀寫緩沖區改值會自動加1
  • limit: 緩存區最大可以進行操作的index位置。緩存區的讀寫狀態正式由這個屬性控制的,可以改變值。
  • capacity: 緩存區的最大容量。這個容量是在緩存區創建時進行指定的。不可以改變值。
  • mark:在當前緩沖區進行position時設置一個標記mark1,當執行buffer的reset方法時,可以把position復位到mark1

selector

1、Selector能夠檢測多個注冊的通道上是否有事件發生(注意:多個Channel 以事件的方式可以注冊到同一個Selector),如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。

2、使用selector檢測,當通道中有事件發生時會主動通知selector,可以減少多線程之間的上下文切換(1個selector系統調用+N次就緒的客戶端的read系統調用)。
傳統做法是需要循環訪問所有通道是否有事件發生,這樣在數據量大的情況下,會導致消耗內存資源嚴重

Selector相關方法說明
selector.select(); //阻塞
selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
selector.wakeup(); //喚醒 selector
selector.selectNow(); //不阻塞,立馬返還

2.3 Selector、Channel 和 Buffer 關系圖

image

  • 每個 Channel 都會對應一個 Buffer。
  • Selector對應一個線程,一個線程對應多個 Channel(連接)。
  • 該圖反應了有三個 Channel 注冊到該 Selector上程序
  • Selector 會根據不同的事件,在各個通道上切換。程序切換到哪個Channel 是由事件決定的,Event 就是一個重要的概念。
  • Buffer 就是一個內存塊,底層是有一個數組。
  • 數據的讀取寫入是通過Buffer,這個和BIO是不同的,BIO中要么是輸入流,或者是輸出流,不能雙向,但是NIO的Buffer是可以讀也可以寫,需要flip方法切換Channel是雙向的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM