天天看點

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

UNIX 系統下的 I/O 模型有 5 種:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路複用、信号驅動 I/O 和異步 I/O。這些名詞我們好像都似曾相識,但這些 I/O 通信模型有什麼差別?同步和阻塞似乎是一回事,到底有什麼不同?等一下,在這之前你是不是應該問自己一個終極問題:什麼是 I/O?為什麼需要這些 I/O 模型?

所謂的 I/O 就是計算機記憶體與外部裝置之間拷貝資料的過程。我們知道 CPU 通路記憶體的速度遠遠高于外部裝置,是以 CPU 是先把外部裝置的資料讀到記憶體裡,然後再進行處理。請考慮一下這個場景,當你的程式通過 CPU 向外部裝置發出一個讀指令時,資料從外部裝置拷貝到記憶體往往需要一段時間,這個時候 CPU 沒事幹了,你的程式是主動把 CPU 讓給别人?還是讓 CPU 不停地查:資料到了嗎,資料到了嗎……

這就是 I/O 模型要解決的問題。今天我會先說說各種 I/O 模型的差別,然後重點分析 Tomcat 的 NioEndpoint 元件是如何實作非阻塞 I/O 模型的。

Java I/O 模型

對于一個網絡 I/O 通信過程,比如網絡資料讀取,會涉及兩個對象,一個是調用這個 I/O 操作的使用者線程,另外一個就是作業系統核心。一個程序的位址空間分為使用者空間和核心空間,使用者線程不能直接通路核心空間。

當使用者線程發起 I/O 操作後,網絡資料讀取操作會經曆兩個步驟:

  • 使用者線程等待核心将資料從網卡拷貝到核心空間
  • 核心将資料從核心空間拷貝到使用者空間

各種 I/O 模型的差別就是:它們實作這兩個步驟的方式是不一樣的。

同步阻塞 I/O:使用者線程發起 read 調用後就阻塞了,讓出 CPU。核心等待網卡資料到來,把資料從網卡拷貝到核心空間,接着把資料拷貝到使用者空間,再把使用者線程叫醒。

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

同步非阻塞 I/O:使用者線程不斷的發起 read 調用,資料沒到核心空間時,每次都傳回失敗,直到資料到了核心空間,這一次 read 調用後,在等待資料從核心空間拷貝到使用者空間這段時間裡,線程還是阻塞的,等資料到了使用者空間再把線程叫醒。

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

I/O 多路複用:使用者線程的讀取操作分成兩步了,線程先發起 select 調用,目的是問核心資料準備好了嗎?等核心把資料準備好了,使用者線程再發起 read 調用。在等待資料從核心空間拷貝到使用者空間這段時間裡,線程還是阻塞的。那為什麼叫 I/O 多路複用呢?因為一次 select 調用可以向核心查多個資料通道(Channel)的狀态,是以叫多路複用。

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

異步 I/O:使用者線程發起 read 調用的同時注冊一個回調函數,read 立即傳回,等核心将資料準備好後,再調用指定的回調函數完成處理。在這個過程中,使用者線程一直沒有阻塞。

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

NioEndpoint 元件

Tomcat 的 NioEndpoint 元件實作了 I/O 多路複用模型,接下來我會介紹 NioEndpoint 的實作原理,下一期我會介紹 Tomcat 如何實作異步 I/O 模型。 

總體工作流程

我們知道,對于 Java 的多路複用器的使用,無非是兩步:

  • 建立一個 Selector,在它身上注冊各種感興趣的事件,然後調用 select 方法,等待感興趣的事情發生。
  • 感興趣的事情發生了,比如可以讀了,這時便建立一個新的線程從 Channel 中讀資料。

Tomcat 的 NioEndpoint 元件雖然實作比較複雜,但基本原理就是上面兩步。我們先來看看它有哪些元件,它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 個元件,它們的工作過程如下圖所示。

NioEndpoint元件:Tomcat如何實作非阻塞I/O?

 LimitLatch 是連接配接控制器,它負責控制最大連接配接數,NIO 模式下預設是 10000,達到這個門檻值後,連接配接請求被拒絕。

Acceptor 跑在一個單獨的線程裡,它在一個死循環裡調用 accept 方法來接收新連接配接,一旦有新的連接配接請求到來,accept 方法傳回一個 Channel 對象,接着把 Channel 對象交給 Poller 去處理。