1. 程式人生 > >四十六、NIO詳解

四十六、NIO詳解

本文你將看到如下內容:

  1. 最好的學習就是教別人。

  2. NIO的用途。

  3. 現代作業系統的5種IO模型。

  4. JavaNIO 的實現及簡單的伺服器例子。

  5. NodeJs的IO模型。

  6. 計算機IO的硬體基礎。

  • 最好的學習就是教別人:

‍首先感謝CDRD給了我這一次分享的機會,為什麼要將感謝放在最開頭。因為這次我選擇java nio 這個主題是因為我自己不理解Java nio的內部實現原理,但是又想學。”最好的學習方式就是教別人”,所以建議大家以後想要學什麼東西,可以嘗試搞清楚以後,與大家分享。

  • 帶著功利心開始我們的學習:談談NIO的用途

當今時代是一個知識資訊爆炸的時代,知識的量幾乎是無限的,如果我們不帶著功利心去學習知識,那麼提升認知的效率將大幅度降低。那麼學習Java NIO能有什麼用呢?

答案是:幫助我們提高IO密集型應用的單機併發量。

何為IO密集型應用?何為CPU密集型應用,這個問題留給大家一起思考。在我們所用到的,或者知道的應用中,有哪些是IO密集型的,又有哪些是CPU密集型的?

為什麼Java NIO能夠提升IO密集型應用的單擊併發量呢?這要從作業系統的IO模型說起。

  • 現代作業系統的IO模型:

對於一次IO訪問(以read舉例),資料會先被拷貝到作業系統核心的緩衝區中,然後才會從作業系統核心的緩衝區拷貝到應用程式的地址空間。所以說,當一個read操作發生時,它會經歷兩個階段:

1. 等待資料準備(Waiting for the data to be ready)

2. 將資料從核心拷貝到程序中(Copying the data from the kernel to the process)

正式因為這兩個階段,linux系統產生了下面五種網路模式的方案。

- 阻塞I/O(blocking IO)

- 非阻塞I/O(nonblocking IO)

- I/O 多路複用(IO multiplexing)

- 訊號驅動I/O(signal driven IO)

- 非同步I/O(asynchronous IO)

1、阻塞I/Oblocking IO

在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

當用戶程序呼叫了recvfrom這個系統呼叫,kernel就開始了IO的第一個階段:準備資料(對於網路IO來說,很多時候資料在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的資料到來)。這個過程需要等待,也就是說資料被拷貝到作業系統核心的緩衝區中是需要一個過程的。而在使用者程序這邊,整個程序會被阻塞(當然,是程序自己選擇的阻塞)。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者程序才解除block的狀態,重新執行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

2、非阻塞I/Ononblocking IO

linux下,可以通過設定socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

當用戶程序發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回一個error。從使用者程序角度講,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的systemcall,那麼它馬上就將資料拷貝到了使用者記憶體,然後返回。

所以,nonblocking IO的特點是使用者程序需要不斷的主動詢問kernel資料好了沒有。

3I/O 多路複用(IO multiplexing

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網路連線的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程序。

當用戶程序呼叫了select,那麼整個程序會被block,而同時,kernel會“監視”所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。

所以,I/O 多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函式就可以返回。

這個圖和blocking IO的圖其實並沒有太大的不同,事實上,還更差一些。因為這裡需要使用兩個systemcall (select 和recvfrom),而blocking IO只調用了一個systemcall (recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

所以,如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用multi-threading+ blocking IO的web server效能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。)

在IO multiplexing Model中,實際中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。

4、非同步I/Oasynchronous IO

Linux下的asynchronous IO其實用得很少。先看一下它的流程:

使用者程序發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了。

5、各種IO模型之間的對比

注:由於signaldriven IO在實際中並不常用,所以我這隻提及剩下的四種IO Model。

POSIX對於阻塞IO和非阻塞IO的定義:

同步IO操作讓IO請求執行緒阻塞,直到IO完成,對應圖中的:Bio、non-blocking IO、multiplexing IO。

非同步IO操作直到IO過程完成都不會讓IO過程阻塞,對應圖中的Aio。

  • Java NIO的底層實現。

讓我們來看一看NIO中的類:

FIleChanel: 用於處理檔案的讀寫。

ServerSocketChannel:用於處理伺服器端TCP連線的接受或拒絕。

SocketChannel:用於處理伺服器端的

DatagramChannel:用於處理UDP報文的收發。

NIO呼叫作業系統的介面是一個典型的IO多路複用函式select,它的呼叫過程如下:

由於篇幅有限,這裡暫時不討論select / poll / epoll的差別,大家知道三者的執行效率:

epoll > poll > select 就行了。

  • NodeJs的IO模型。

讓我們換一個角度看待這個問題,還有什麼底層的IO模型,跟java Nio是類似的呢,答案是java。

Java的執行環境為什麼是單執行緒的呢?

Js最初設計是執行在瀏覽器中的,而瀏覽器中的每一個window只會為js開一個執行緒來執行,瀏覽器是事件驅動的,所有的操作都被加入事件佇列,由一個執行緒專門輪訓處理。在js中,沒有類似於java一樣的sleep函式。所有的操作都是非同步的,包括IO操作在內。

由此誕生了鼎鼎大名的nodeJs,用nodeJs寫的伺服器,所有的IO請求都由底層的EventLoop代為處理。

用這種方式寫的web伺服器跟我們平時用得多的tomcat伺服器的區別在於,這種伺服器的IO請求都交給event loop進行處理,而tomcat是處理使用者請求的執行緒線上程池裡面等,這樣的話,由於cpu線上程之間來回切換會有開銷,所以對於IO密集型的應用來說,以IO複用,模型為基礎的伺服器的單機併發量要高一些。

廢話少說,上一組測試資料:

(資料來源:https://dzone.com/articles/performance-comparison-between)

Node

JavaEE

第一列代表併發數,第二列代表返回時間,第三列代表每秒處理請求的數量,可以看出node伺服器的併發處理速度大約比java快了20%。

  • IO的硬體基礎。

讓我們一起再往底層看一看,現代計算機的一次IO過程低如何完成的。

現代計算機的IO過程如圖:

1.作業系統下發一個IO請求。

2.DMA晶片向CPU傳送一個接管匯流排的請求,匯流排一旦被DMA佔據,CPU和其他裝置都不能再佔有匯流排去做其他事。

3.CPU將匯流排控制權交給DMA晶片。

4.DMA拿到匯流排控制權以後,告訴外部快取裝置可以開始傳送資料。

5.IO裝置快取將資料拷貝到記憶體中。

可以看出,整個IO過程主要是由DMA來做的,這樣的目的是為了不佔據CPU的時間片,降低CPU的開銷。

事實上,我們在select函式中獲得的通知,正是在DMA將資料拷貝的作業系統kernel裡面後,觸發的一箇中斷。告訴我們說IO傳輸已經完成,之後再由作業系統來將資料拷貝到使用者空間(AIO),或者通知由使用者自取(BIO,IO複用)。

總結:

這一次我們從多個角度看到了現代計算機IO發生的過程。回顧一下:

1.計算機的IO是發生的兩個過程是什麼呢?

2.由這兩個過程演化出來的4種IO模型是哪4種呢?

3.IO多路複用的優勢在哪兒呢?

4.將單擊IO的情況遷移到分散式場景裡面,或者資料庫IO裡面,你能想出有哪些類似的場景呢?