I/O模型概述
本文將介紹核心空間與使用者空間的區別,I/O模型的分類和原理。
使用者空間和核心空間
在學習I/O之前,有必要了解使用者空間和核心空間的概念,因為所有的I/O操作都牽涉到使用者空間到核心空間的切換。下面看下什麼是使用者空間和核心空間:
1、使用者空間
使用者空間是常規程序所在的區域,什麼是常規程序,開啟工作管理員看到的就是常規程序:

JVM也屬於常規程序,駐守於使用者空間,使用者空間是非特權區域,比如在該區域執行的程式碼不能直接訪問硬體裝置。
2、核心空間
核心空間主要是指作業系統執行時所使用的用於程式排程、虛擬記憶體的使用或者連線硬體資源等的程式邏輯。核心程式碼有特別的權利,比如它能與裝置控制器通訊,控制著整個用於區域程序的執行狀態。
為什麼要劃分使用者空間和核心空間?主要是為了保證作業系統的穩定性和安全性。使用者程式不可以直接訪問硬體資源,如果使用者程式需要訪問硬體資源,必須呼叫作業系統提供的介面,這個呼叫介面的過程也就是系統呼叫。每一次系統呼叫都會存在兩個記憶體空間之間的相互切換,通常的網路傳輸也是一次系統呼叫,通過網路傳輸的資料先是從核心空間接收到遠端主機的資料,然後再從核心空間複製到使用者空間,供使用者程式使用。

如何分配使用者空間和核心空間的比例也是一個問題,是更多地分配給使用者空間供使用者程式使用,還是首先保住核心有足夠的空間來執行,還是要平衡一下。在當前的Windows 32位作業系統中,預設使用者空間:核心空間的比例是1:1,而在32位Linux系統中的預設比例是3:1(3GB使用者空間、1GB核心空間)。
同步和非同步、阻塞和非阻塞
IO的分類包括:同步IO、非同步IO、阻塞IO和非阻塞IO。
1、同步和非同步
同步和非同步這個概念比較廣,不僅僅是在I/O,其他的還有諸如同步呼叫/非同步呼叫、同步請求/非同步請求,都是一個意思。同步和非同步,關注的是訊息通訊機制。
所謂同步,就是在發出一個"呼叫請求"時,在沒有得到結果之前,該"呼叫請求"就不返回。換句話說,就是由"呼叫者"主動等待"呼叫"的結果。像我們平時寫的,方法A呼叫Math.random()方法、方法B呼叫String.substring()方法都是同步呼叫,因為呼叫者主動在等待這些方法的返回。
所謂非同步,則正好相反,"呼叫"發出之後,這個呼叫就直接返回了,所有沒有返回結果。換句話說,當一個非同步呼叫請求發出之後,呼叫者不會立刻得到結果。
2、阻塞和非阻塞
阻塞和非阻塞關注的是程式在等待呼叫結果時的狀態。
阻塞呼叫指的是呼叫結果返回之前,當前執行緒會被掛起,呼叫執行緒只有在得到結果之後才會返回。
非阻塞呼叫指的是在不能立即得到結果之前,該呼叫不會阻塞當前執行緒。
Linux網路I/O模型
由於絕大多數的Java應用都部署在Linux系統上,因此這裡談一下Linux網路I/O模型。分為5類:
1、阻塞I/O模型
阻塞I/O模型就是最常用的I/O模型,預設情況下所有的檔案操作都是阻塞的,以Socket來講解此模型:在使用者空間中呼叫recvfrom,其系統呼叫直到資料包到達且被複制到應用程序的緩衝區或者發生錯誤時才返回,在此期間會一直等待,程序在從呼叫recvfrom開始到它返回的整段時間內都是被阻塞的,因此被稱為阻塞I/O。

2、非阻塞I/O模型
recvfrom從使用者空間到核心空間的時候,如果該緩衝區沒有資料的話,就直接返回一個EWOULDBOCK錯誤,一般都對非阻塞I/O模型進行輪詢檢查這個狀態,看核心空間是不是有資料到來,有資料到來則從核心空間複製資料到使用者空間。
3、I/O複用模型
Linux提供select/poll,程序通過將一個或者多個fd傳遞給select或poll系統呼叫,阻塞在select操作上,這樣select/poll可以幫助我們偵測多個fd是否處於就緒狀態。select/poll是順序掃描fd是否就緒,而且支援的fd數量有限,因此它的使用受到了一些制約。Linux還提供了一個epoll系統呼叫,epoll使用基於事件驅動方式替代順序掃描,因此效能更高。當有fd就緒時,立即會掉函式callback。

4、訊號驅動I/O模型
首先開啟Socket訊號驅動I/O功能,並通過系統呼叫sigaction執行一個訊號處理函式(此係統呼叫立即返回,程序繼續工作,它是非阻塞的)。當資料準備就緒時,就為程序生成一個SIGIO訊號,通過訊號會掉通知應用程式呼叫recvfrom來讀取資料,並通知主迴圈函式來處理資料。

5、非同步I/O
告知核心啟動某個操作,並讓核心在整個操作完成後(包括將資料從核心複製到使用者自己的緩衝區)通知開發者。這種模型與訊號驅動I/O模型的主要區別是:訊號驅動I/O模型由核心通知應用程式何時可以開始一個I/O操作,非同步I/O模型由核心通知應用程式I/O操作何時已經完成。

BIO與NIO
BIO:同步阻塞IO。對應有兩種Java模型,一種是1:1模型,一個連 接 對應一個執行緒;另外一種是M:N模型。如下圖所示:

1:1 BIO

M:N BIO
NIO:同步非阻塞IO,對應Java模型如下圖所示:

NIO與BIO的主要區別:
在BIO中,等待客戶端傳送資料這個過程是阻塞的,這就造成了一個執行緒只能處理一個請求的情況,而機器能支援的最大執行緒數是有限的,這就是為什麼BIO不能支援高併發的原因
在NIO中,當一個Socket建立好之後,Thread並不會去阻塞接收這個Socket,而是將這個請求交給Selector,Selector會判斷哪個Socket建立完成,然後通知對應執行緒,對應執行緒處理完資料再返回給客戶端,這樣就可以讓一個執行緒處理更多的請求了
在NIO上,Selector做的事情就是以單條執行緒監視多Socket I/O的狀態,空閒時阻塞當前執行緒,當有一個或者多個Socket有I/O事件時就從阻塞狀態中醒來。