一篇文章帶你徹底搞懂NIO

本篇文章目的在於基本概念和原理的解釋,不會貼過多的使用代碼。

什麼是NIO

Java NIO (New IO)是 Java 的另一個 IO API (來自 java1.4) ,意味着可以替代標準的 Java IO API和 Java Networking API。 提供了一種與標準 IO API 不同的 IO 工作方式。

注意:Java的NIO只是說IO API,阻塞非阻塞纔是IO的模型。

也有人稱NIO爲No-Blocking IO,非阻塞IO,但是這麼說並不嚴謹。因爲對於基礎的IO操作API(比如文件IO,FileChannel),還是阻塞的模型。只有對Networking IO API纔可以使用非阻塞的模型(configureBlocking(false))。

Java NIO中的Networking IO API,支持非阻塞IO模型,還實現了IO多路複用(IO Multiplexing)。對於服務端來說,可以用更少的線程支持更多的併發,大幅度提升了性能。

NIO中的阻塞與非阻塞

阻塞與非阻塞是從線程的角度出發的,這裏指的是線程狀態。

阻塞

當進行IO讀寫時,線程是阻塞的狀態。此時會讓出cpu控制權,不會佔用cpu資源。

什麼?不佔用CPU資源?那是不是代表阻塞模型更好呢?

答案是並不是,雖然阻塞狀態不會佔用CPU,但是會發生線程的切換,線程切換時會有上下文保存轉換的過程,需要CPU調度,是一個很昂貴的操作。

Java NIO中的基礎IO API(非Networking IO API)還是阻塞的方式,只是使用方式從面向流(stream)編程面向塊(buffer)了,和BIO本質上並沒有什麼區別。

非阻塞

非阻塞是指在進行IO操作的時候,如果設備還未準備好(比如socket還沒有收到數據),操作會直接返回結果,不會讓當前線程進入阻塞狀態。

這樣的優點是,使用者可以自行決定在數據未準備好時的操作。線程可以在沒有數據期間去執行其他操作。
Networking API可以配置爲非阻塞模型Channel.configureBlocking(false),配合Selector來實現多路複用功能。簡單的說就是一個Selector監聽多個socket io(對於unix系統來說,socket也是一個fd,也屬於io),可以在一個線程中支持多個連接。當然在實際服務器開發時,就算是NIO模型,有些程序也不會只使用一個線程;但相比傳統的Blocking IO方式來說,需要的線程數量也會大大減少了。(redis中就是使用了IO多路複用技術,並且只有一個線程監聽socket io)

AIO

AIO 是 Java 1.7 之後引入的包,是 NIO 的升級版本,新增了提異步非阻塞的 IO 操作方式,所以人們叫它 AIO(Asynchronous IO),異步 IO 是基於事件和回調機制實現的,也就是應用操作之後會直接返回,不會堵塞在那裏,當後臺處理完成,操作系統會執行回調通知相應的線程進行後續的操作。

多路複用

在I/O編程過程中,當需要同時處理多個客戶端請求時,可以利用多線程或者I/O多路複用技術進行處理。I/O多路複用技術通過把多個I/O的阻塞複用到同一個Select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型相比,I/O多路複用的最大優勢是系統開銷小,系統不需要創建新的額外進程或者線程,也不需要維護這些線程和進程的運行,降低了系統的維護工作量,節省了系統的資源,I/O多路複用的主要應用場景如下:

  • 服務器需要同時處理多個處於監聽狀態或者多個連接狀態的Socket
  • 服務器需要同時處理多種網絡協議的Socket

目前支持I/O多路複用的系統調用又select/pselect/poll/epoll。

select/epoll

select

select的實現思路很直接。假如程序同時監視如下圖的sock1、sock2和sock3三個socket,那麼在調用select之後,操作系統把進程A分別加入這三個socket的等待隊列中。

操作系統把進程A分別加入這三個socket的等待隊列中

當任何一個socket收到數據後,中斷程序將喚起進程。下圖展示了sock2接收到了數據的處理流程。

sock2接收到了數據中斷程序喚起進程A

所謂喚起進程,就是將進程從所有的等待隊列中移除,加入到工作隊列裏面。如下圖所示。

將進程A從所有等待隊列中移除再加入到工作隊列裏面

經由這些步驟,當進程A被喚醒後,它知道至少有一個socket接收了數據。程序只需遍歷一遍socket列表,就可以得到就緒的socket。

這種簡單方式行之有效,在幾乎所有操作系統都有對應的實現。

但是簡單的方法往往有缺點,主要是:

其一,每次調用select都需要將進程加入到所有監視socket的等待隊列,每次喚醒都需要從每個隊列中移除。這裏涉及了兩次遍歷,而且每次都要將整個fds列表傳遞給內核,有一定的開銷。正是因爲遍歷操作開銷大,出於效率的考量,纔會規定select的最大監視數量,默認只能監視1024個socket。

其二,進程被喚醒後,程序並不知道哪些socket收到數據,還需要遍歷一次。

那麼,有沒有減少遍歷的方法?有沒有保存就緒socket的方法?這兩個問題便是epoll技術要解決的。

補充說明: 本節只解釋了select的一種情形。當程序調用select時,內核會先遍歷一遍socket,如果有一個以上的socket接收緩衝區有數據,那麼select直接返回,不會阻塞。這也是爲什麼select的返回值有可能大於1的原因之一。如果沒有socket有數據,進程纔會阻塞。

select低效的原因之一是將“維護等待隊列”和“阻塞進程”兩個步驟合二爲一。如下圖所示,每次調用select都需要這兩步操作,然而大多數應用場景中,需要監視的socket相對固定,並不需要每次都修改。epoll將這兩個操作分開,先用epoll_ctl維護等待隊列,再調用epoll_wait阻塞進程。顯而易見的,效率就能得到提升。

image

select低效的另一個原因在於程序不知道哪些socket收到數據,只能一個個遍歷。如果內核維護一個“就緒列表”,引用收到數據的socket,就能避免遍歷。如下圖所示,計算機共有三個socket,收到數據的sock2和sock3被rdlist(就緒列表)所引用。當進程被喚醒後,只要獲取rdlist的內容,就能夠知道哪些socket收到數據。

image

epoll

epoll是在select出現N多年後才被髮明的,是select和poll的增強版本。epoll通過以下一些措施來改進效率。

原理:

創建epoll對象

如下圖所示,當某個進程調用epoll_create方法時,內核會創建一個eventpoll對象(也就是程序中epfd所代表的對象)。eventpoll對象也是文件系統中的一員,和socket一樣,它也會有等待隊列。

image

創建一個代表該epoll的eventpoll對象是必須的,因爲內核要維護“就緒列表”等數據,“就緒列表”可以作爲eventpoll的成員。

維護監視列表

創建epoll對象後,可以用epoll_ctl添加或刪除所要監聽的socket。以添加socket爲例,如下圖,如果通過epoll_ctl添加sock1、sock2和sock3的監視,內核會將eventpoll添加到這三個socket的等待隊列中。

添加所要監聽的socket

當socket收到數據後,中斷程序會操作eventpoll對象,而不是直接操作進程。

接收數據

當socket收到數據後,中斷程序會給eventpoll的“就緒列表”添加socket引用。如下圖展示的是sock2和sock3收到數據後,中斷程序讓rdlist引用這兩個socket。

![給就緒列表添加引用
](https://pic1.zhimg.com/80/v2-...

eventpoll對象相當於是socket和進程之間的中介,socket的數據接收並不直接影響進程,而是通過改變eventpoll的就緒列表來改變進程狀態。

當程序執行到epoll_wait時,如果rdlist已經引用了socket,那麼epoll_wait直接返回,如果rdlist爲空,阻塞進程。

阻塞和喚醒進程

假設計算機中正在運行進程A和進程B,在某時刻進程A運行到了epoll_wait語句。如下圖所示,內核會將進程A放入eventpoll的等待隊列中,阻塞進程。
epoll_wait阻塞進程

當socket接收到數據,中斷程序一方面修改rdlist,另一方面喚醒eventpoll等待隊列中的進程,進程A再次進入運行狀態(如下圖)。也因爲rdlist的存在,進程A可以知道哪些socket發生了變化。

epoll喚醒進程

參考

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