I/O模型
在開始NIO的學習之前,先對I/O的模型有一個理解,這對NIO的學習是絕對有好處的。我畫一張圖,簡單表示一下數據從外部磁盤向運行中進程的內存區域移動的過程:
這張圖片明顯忽略了很多細節,只涉及了基本操作,下面分析一下這張圖。
用戶空間和內核空間
一個計算機通常有一定大小的內存空間,如一台計算機有4GB的地址空間,但是程序並不能完全使用這些地址空間,因為這些地址空間是被劃分為用戶空間和內核空間的。程序只能使用用戶空間的內存,這裡所說的使用是指程序能夠申請的內存空間,並不是真正訪問的地址空間。下面看下什麼是用戶空間和內核空間:
1、用戶空間
用戶空間是常規進程所在的區域,什麼是常規進程,打開任務管理器看到的就是常規進程:
JVM就是常規進程,駐守於用戶空間,用戶空間是非特權區域,比如在該區域執行的代碼不能直接訪問硬件設備。
2、內核空間
內核空間主要是指操作系統運行時所使用的用於程序調度、虛擬內存的使用或者連接硬件資源等的程序邏輯。內核代碼有特別的權利,比如它能與設備控制器通訊,控制著整個用於區域進程的運行狀態。和I/O相關的一點是:所有I/O都直接或間接通過內核空間。
那麼,為什麼要劃分用戶空間和內核空間呢?這也是為了保證操作系統的穩定性和安全性。用戶程序不可以直接訪問硬件資源,如果用戶程序需要訪問硬件資源,必須調用操作系統提供的接口,這個調用接口的過程也就是系統調用。每一次系統調用都會存在兩個內存空間之間的相互切換,通常的網絡傳輸也是一次系統調用,通過網絡傳輸的數據先是從內核空間接收到遠程主機的數據,然後再從內核空間復制到用戶空間,供用戶程序使用。這種從內核空間到用戶控件的數據復制很費時,雖然保住了程序運行的安全性和穩定性,但是犧牲了一部分的效率。
最後,如何分配用戶空間和內核空間的比例也是一個問題,是更多地分配給用戶空間供用戶程序使用,還是首先保住內核有足夠的空間來運行,還是要平衡一下。在當前的Windows 32位操作系統中,默認用戶空間:內核空間的比例是1:1,而在32位Linux系統中的默認比例是3:1(3GB用戶空間、1GB內核空間)。
進程執行I/O操作的步驟
緩沖區,以及緩沖區如何工作,是所有I/O的基礎。所謂"輸入/輸出"講的無非也就是把數據移入或移出緩沖區。
進程執行I/O操作,歸結起來,就是向操作系統發出請求,讓它要麼把緩沖區裡的數據排干淨(寫),要麼用數據把緩沖區填滿(讀)。進程利用這一機制處理所有數據進出操作,操作系統內部處理這一任務的機制,其復雜程度可能超乎想像,但就概念而言,卻非常直白易懂,從上面的圖,可以總結一下進程執行I/O操作的幾步:
1、進程使用底層函數read(),建立和執行適當的系統調用,要求其緩沖區被填滿,此時控制權移交給內核
2、內核隨即向磁盤控制硬件發出命令,要求其從磁盤讀取數據
3、磁盤控制器和數據直接寫入內核內存緩沖區,這一步通過DMA完成,無需主CPU協助。這裡多提一句,關於DMA,可以百度一下,它是現代電腦的重要特色,它允許不同速度的硬件裝置來溝通,而不需要依賴於CPU的大量中斷負載,大大提升了整個系統的效率
4、一盤磁盤控制器把緩沖區填滿,內核隨即把數據從內核空間的臨時緩沖區拷貝到進程執行read()調用時指定的緩沖區
5、進程從用戶空間的緩沖區中拿到數據
當然,如果內核空間裡已經有數據了,那麼該數據只需要簡單地拷貝出來即可。至於為什麼不能直接讓磁盤控制器把數據送到用戶空間的緩沖區呢?最簡單的一個理由就是,硬件通常不能直接訪問用戶空間。
同步和異步、阻塞和非阻塞
有了上面對於I/O的理解,我們就可以理解一下同步和異步的區別了。
同步和異步的關注點是用戶線程和內核的交互方式。同步指的是用戶線程發起I/O請求後需要等待或者輪詢內核I/O操作完成後才能繼續執行;異步是指用戶線程發起I/O請求後仍然繼續執行,當內核I/O操作完成後會通知用戶線程,或者調用用戶線程注冊的回調函數。
阻塞和非阻塞的關注點是用戶線程調用內核操作的方式。阻塞是指I/O操作需要徹底完成後才返回到用戶空間,非阻塞是指I/O操作被調用後立即返回給用戶一個狀態值,無需等到I/O操作徹底完成。
同步阻塞I/O
同步阻塞I/O模型,最簡單的I/O模型,用戶線程在內核進行I/O操作時被阻塞。
同步阻塞I/O的操作為,用戶線程通過系統調用read()函數,發起I/O讀操作,由用戶空間轉到內核空間。內核等到數據包到達之後,然後將接收到的數據拷貝到用戶空間,完成read()操作。整個過程中,用戶線程需要等待read()函數將數據讀取到用戶空間緩沖區,才能夠繼續處理接收的數據。這將導致用戶線程發起I/O請求時,不能做任何事情,對CPU的資源利用率不夠。
同步非阻塞I/O
同步非阻塞I/O模型是建立在同步阻塞I/O模型的基礎上的,用戶線程發起I/O請求之後可以立即返回。
同步非阻塞I/O的操作,上面已經說了,用戶線程發起I/O請求之後立即返回,但此時並未讀取到任何數據,用戶線程需要不斷發起I/O請求,直到數據到達之後,才真正地讀到數據,繼續執行。整個過程中,用戶需要不斷地調用read(),嘗試是否可以讀取成功,讀取成功才繼續處理接收的數據。這樣,雖然用戶線程每次發起I/O請求後可以立即返回,但是這並沒有什麼意義,為了等到數據,仍然需要不斷輪詢、重復請求,消耗了大量的CPU資源,因此一般很少使用這種模型。
I/O多路復用
I/O多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞I/O模型中輪詢等待的問題。
I/O多路復用的操作位,用戶線程首先將需要進行的I/O操作添加到select中,然後阻塞等待select系統調用返回。當數據到達時,I/O操作被激活,select函數返回,用戶線程正式發起read()請求,讀取數據並繼續執行。
從流程上來看,使用select函數進行I/O請求和同步阻塞模型沒有太大區別甚至還多了添加監視I/O,以及調用select函數的額外操作,效率更差。但是,使用select函數以後的最大優勢就是可以在一個線程內同時處理多個I/O請求。用戶可以注冊多個I/O,然後不斷地調用select讀取被激活的I/O,即可達到在同一個線程內同時處理多個I/O請求的目的。而在同步阻塞模型中,用戶必須通過多線程的方式才能達到這個目的地。
然而,使用select函數的優點並不僅限於此。雖然上述方式允許單線程內處理多個I/O請求,但是每個I/O請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞I/O模型還要長。如果用戶線程只注冊自己感興趣的I/O請求,然後去做自己的事情,等到數據到來時再進行處理,那麼則可以提高CPU的利用率。
I/O多路復用模型是最常使用的I/O模型,但是其異步程度還不夠徹底,因為它使用了會阻塞線程的select系統調用。因此I/O多路復用模型只能稱為異步阻塞I/O,而非真正的異步I/O。
Java NIO2:緩沖區 http://www.linuxidc.com/Linux/2016-01/127770.htm