IO 和 NIO 的思考

輸入輸出是操作系統不可或缺的一部分,大致分爲兩類:面向磁盤和麪向網絡。在 Java 中有3種 I/O 類型:BIO、NIO 和 AIO,分別是同步阻塞、同步非阻塞和異步非阻塞 I/O,這裏着重描述 BIO 和 NIO 的區別和常用的編程模型。

1. 爲什麼設計 NIO

一個直接原因就是爲了更好的利用操作系統特性,改善和擴展原有 API。與 NIO 相關的規範有兩個:

  • JSR 51:它是 NIO 的第一個規範,關注緩衝區、通道和字符集的設計,引入一個簡單的面向緩衝區的 I/O 模型,並且提供一套非阻塞、I/O 多路複用、可擴展的 API
  • JSR 203(NIO.2):它在前者的基礎上,添加新的文件系統的抽象,完善現有 Socket 通道的配置,添加多播數據報的支持,並且定義了一個異步 I/O 編程 API

那麼,傳統的 BIO 又有什麼弊端?NIO 又是如何改進的?可以從兩方面進行說明。

1.2 文件操作

關於 java.io.file,它的不足之處在於:

  • 查詢文件屬性時,如修改時間或文件類型,都會發生系統調用,並且這些組合操作非常常見,造成性能問題
  • 部分方法在發生錯誤時返回 false 而不是拋出異常,比如 delete、rename,不知操作失敗的原因
  • 一些 OS 高級功能不支持,比如符號鏈接、文件鎖定、內存映射等

而 NIO 支持批量獲取文件屬性,對文件、目錄的處理也重新設計,提供 FileLock、MappedByteBuffer 等支持 OS 高級功能。

1.3 網絡通信

BIO 是同步阻塞、基於流的 I/O,阻塞就意味着當 Socket 輸入流中無數據可讀取時,調用線程掛起,直到有數據讀取,期間不能處理其他請求,如果來了一個新連接,就只能再新建一個線程處理。

隨着連接數的增加,BIO 將會創建大量線程,而一個計算機能打開的進程數或線程數是有限的,嚴重的時候可能會導致應用崩潰無響應。一個有效的解決辦法是使用線程池,限制最大線程數,但它同時也限制了最大連接數。

NIO 將讀寫改爲非阻塞,無數據可讀,線程返回線程池,可用於處理其他連接。它對原始 I/O 提供了新的抽象 - Channel(通道),並且提供基於緩衝區的讀寫 API。Channel 表示一個到硬件設備、文件或網絡套接字的連接,與 java.net.Socket 的區別是:

  • 可配置非阻塞,允許事件驅動的設計,提供了一種更加可擴展的服務器開發
  • 面向緩衝區,可實現零拷貝執行 I/O ,只不過有一端必須是 FileChannel

相同環境下,BIO 的線程全程只處理一條連接,而 NIO 的線程可處理多個連接,提高了系統的吞吐能力。NIO 在服務器進行縱向擴展(比如增加內存、CPU)或者橫向擴展(比如增加服務器)往往能夠比 BIO 帶來更高的處理能力,使服務器具有更強的可擴展性和可伸縮性。

1.4 零拷貝

NIO 還有一個零拷貝的概念,零拷貝是指 CPU 不執行將數據從一個存儲區複製到另一個存儲區的操作。OS 級別的零拷貝指的是將數據發送到硬件驅動程序(網卡或磁盤驅動器)時避免從一個位置複製到另一個位置(一般是從用戶空間到內核空間),反之亦然。NIO 中的零拷貝就是這樣,只不過它只針對在網絡上發送文件。

2. I/O 模型的選擇

一般的,我們潛意識的會認爲 NIO 比 BIO 的性能高,其實不盡然,當然了有個讀取方式的問題,read(byte[]) 和 read(ByteBuffer)應該沒區別吧?所以如果系統的併發量不高,兩個用誰都行。

BIO 的問題通常會在海量的連接下體現出來,由於它不能充分利用、壓榨一臺服務器的性能,不管怎麼擴展,它能處理的連接數與機器性能往往是非線性的,付出和收穫不成正比。如果你的應用面臨的連接不斷增加,特別是存在大量的長連接,此時就要選擇 NIO,它不僅提高了單機處理能力,還能節省服務器成本。

NIO 相比 BIO 的重點在於可擴展性,在選擇 I/O 模型時,需要結合業務場景,綜合考慮以下幾點:

  • 預計最大的併發數
  • 短連接還是長連接
  • 預計每個連接的數據量,即流量的大小
  • NIO 靈活,但代價是編程複雜

3. 編程模型

BIO 的編程模型是一連接一處理線程,採用線程池優化。

NIO 典型的編程模型是 Reactor,事件複用器通知套接字何時準備好讀取和寫入操作的事件,將事件傳遞給合適的處理程序,由該程序負責實際的讀取或寫入。對於讀操作基本過程如下:

  • 處理程序聲明感興趣的 I/O 事件 - 讀取事件
  • 事件複用器等待事件
  • 一個事件發生,複用器被喚醒並調用適當的處理程序
  • 處理程序執行實際的讀取操作,處理讀取的數據,重新聲明關注的 I/O 事件,並將控制權返回給調度程序

與 Reactor 相對的還有一個 AIO 的 Proactor 模型,它是異步 I/O,事件複用器等待 I/O 操作完成的事件,它是真正的異步,因爲實際的 I/O 操作完全由操作系統執行。對於讀操作,它的做法是:

  • 處理程序啓動異步讀取操作,在這種情況下,處理程序不關心 I/O 就緒事件,而是關注接收完成的事件
  • 事件複用器等待操作完成
  • 當事件複用器等待時,OS 並行的在內核線程中執行讀操作,將數據放入用戶定義的緩衝區,讀取完成後通知事件多路複用器
  • 事件複用器調用適當的處理程序
  • 處理程序處理用戶定義緩衝區的數據,啓動新的異步操作,並將控制返回給事件多路複用器

4. 小結

I/O 模型經常對比的是阻塞和非阻塞,還有同步和異步,在《UNIX 網絡編程》第6章已經給出了它們的區別,並且給出的圖示很直觀。這裏簡單做下說明,以 UDP 爲例,讀取一個數據包可以分爲兩個階段:

  • 第一階段:應用進程發起系統調用,內核無數據包準備好,等待數據
  • 第二階段:數據包準備好,OS 將數據從內核複製到用戶空間

阻塞和非阻塞描述的是第一階段無數據可讀取時線程是否掛起;同步和異步描述的是第二階段,在數據複製過程中線程是否參與和掛起。注意 NIO/BIO 都是同步 I/O,NIO 對應 UNP 中描述的 I/O 複用模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章