Java NIO之理解I/O模型(二)

前言

上一篇文章講解了I/O模型的一些基本概念,包括同步與異步,阻塞與非阻塞,同步IO與異步IO,阻塞IO與非阻塞IO。此次一塊兒來了解一下現有的幾種IO模型,以及高效IO的兩種設計模式,也都是屬於IO模型的基礎知識。html

UNIX下可用的五種I/O模型

根據UNIX網絡編程對IO模型的分類,UNIX提供了5種IO模型,下面分別來介紹一下。linux

阻塞I/O模型

最多見的一種IO模型,以前介紹過,一個read操做是分兩個階段的,第一個階段是,等待數據準備就緒,第二個階段是將數據拷貝到調用這個IO的線程中。阻塞是發生在第一個階段的,當數據沒有準備好時,會一直阻塞用戶線程,當數據就緒後再將數據拷貝到線程中,並返回結果給用戶線程。編程

大體過程以下圖。設計模式

其實,大部分的socket接口都是典型的阻塞型。所謂阻塞型的接口是指系統調用(通常是IO接口)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用得到結果或者超時出錯時才返回。緩存

經過介紹了阻塞IO,咱們很容易就會發現它的問題,那就是阻塞會是用戶線程沒法進行任何運算和請求。通常咱們的處理這種問題的狀況是使用多線程,每一個連接建立一個線程,或是使用線程池來管理線程,或許能夠緩解部分壓力,可是不能解決全部問題。多線程模型能夠方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,能夠用非阻塞接口來嘗試解決這個問題。網絡

非阻塞I/O模型

非阻塞IO模型是這樣一個過程,當應用程序發起一個read操做時,並不會阻塞,而是馬上會收到一個結果。應用程序的線程發現返回結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦數據準備好了,而且又再次收到了用戶線程的請求,那麼它立刻就將數據拷貝到了用戶內存,而後返回。多線程

這樣的一個過程,實際上是須要用戶線程不斷的去詢問系統是否準備好了數據,這樣就會一直佔用CPU資源。可是這種模型是在只專門提供某種功能的系統纔有。異步

大體過程以下:
socket

多路I/O複用模型

在介紹多路複用I/O時就要先簡單說明一下,select函數和poll函數。函數

select函數

select函數容許進程指示內核等待多個事件中的任何一個事件發生,而且只在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。

  • 舉個例子,咱們能夠調用select,告知內核僅在下列狀況發生時才返回:
  • 集合 {1,4,5} 中的任何描述符準備好讀;
  • 集合 {2,7} 中的任何描述符準備好寫;
  • 集合 {1,4} 中的任何描述符有異常條件待處理;
  • 已經經歷10.2秒;

也就是說,咱們調用select告知內核對哪些描述符(讀、寫或異常條件)感興趣以及等待多長時間。

poll函數

poll函數起源於SVR3,最初侷限於流設備。SVR4取消了這種限制,容許poll工做在任何描述符上。poll函數提供的功能與select函數相似,可是poll沒有最大文件描述符數量的限制。

select函數和poll函數將就緒的文件描述符告訴進程後,若是進程沒有對其進行IO操做,那麼下次調用select函數或者poll函數時會再次報告這些文件描述符, 因此他們通常不會丟失就緒的消息,這種方式稱爲水平觸發(Level Triggered)。

簡單的解了select函數和poll函數後,下面咱們就繼續說多路I/O複用模型。多路IO複用模型就是調用select或poll函數,而且此模型的阻塞過程就是發生在調用這兩個函數中的,而不是發生在真正的的I/O系統調用上的,使用select或poll的好處在於能夠用單個線程或進程,處理多個網絡鏈接的IO。整個過程就是select或poll函數會不斷的輪詢所負責的socket,當某個socket有數據到達了,就通知用戶線程或進程。

大概調用以下:

Java中的NIO實際上就是使用的多路IO複用模型,經過selector.select()去查詢每一個通道是否有到達事件,若是沒有事件,則一直阻塞在那裏,所以多路複用IO模型也會阻塞用戶線程,只不過線程是被select函數阻塞的而不是被scoket IO阻塞的。

因此多路複用IO模型和非阻塞IO有相似之處,可是多路複用IO模型的效率是比非阻塞IO模型要高的,由於在非阻塞IO中,不斷的詢問scoket狀態的是經過用戶線程去進行的,而多路複用IO模型,輪詢每一個scoket狀態是內核在進行的,這個效率是比用戶線程要高不少的。這樣也能看出來多路複用IO模型比較適合連接數比較多的狀況。

不過此模型也是存在問題的,因爲多路複用IO模型是經過輪詢的方式來檢測是否有事件到達,並對到達的事件逐一響應,一旦事件響應體很大或是響應事件數量過多,就會消耗大量的時間去處理事件,從而影響整個過程的及時性。爲了應對這種狀況linux系統提供了epoll接口,可是除了linux的其餘操做系統對epoll接口的支持又有不少差別,因此雖然epoll解決了事件檢測的時效性問題,可是在跨平臺能力上卻並不能獲得很好的支持。

信號驅動IO模型

在信號驅動IO模型中,讓內核在數據報準備就緒時發送SIGIO信號通知用戶線程。

整個過程以下:

首先開啓套接字的信號驅動式IO功能,並經過sigaction系統調用安裝一個信號處理函數。該系統調用將當即返回,進程繼續工做,也就是說沒有被阻塞。當數據報準備好讀取時,內核就爲該進程產生一個SIGIO信號。咱們隨後就能夠在信號處理函數中調用recvfrom讀取數據報,並通知用戶進程數據已經準備好了,能夠讀取了。

這種模型的優勢在於等待數據報到達期間不會被阻塞,用戶進程能夠繼續執行,只要等待來自信號處理函數的通知便可。

異步IO模型

異步IO模型的過程是這樣的,當用戶線程發起read操做時,告知內核啓動讀取數據操做,並讓內核在整個操做(包括將數據從內核複製到咱們本身的緩衝區)完成後通知咱們。這樣在內核執行讀取數據操做時,用戶線程能夠繼續執行,當接收到內核在整個操做都完成的信號時,就能夠直接去使用數據了。

大體過程以下:

在異步IO模型中,IO操做的兩個階段都不會阻塞用戶線程或進程,這兩個階段都是由內核完成的,而後發送一個信號告知用戶線程或進程操做已完成。異步IO模型與信號驅動IO模型的區別在於,信號驅動IO模型是由內核通知用戶線程什麼時候啓動一個IO操做,而異步IO模型是由內核通知咱們IO操做什麼時候完成,異步IO模型中用戶線程並不須要進行實際的讀寫操做,只須要在內核操做完成後,接到讀取完成信號後,直接使用數據便可。

異步IO是須要操做系統底層支持的,Linux從內核2.6版本纔開始支持異步IO。在Java 7中就已經支持異步IO了。

兩種高性能IO設計模式Reactor和Proactor

Reactor模式

Reactor的意思是反應器,字面意思就是當即反應。

Reactor的工做方式:

(1)應用程序註冊讀就緒事件和相關聯的事件處理器

(2)Reactor阻塞等待內核事件通知

(3)Reactor收到通知,而後分發可讀寫事件(讀寫準備就緒)到用戶事件處理函數

(4)用戶讀取數據,並處理數據

(5)事件處理器完成實際的讀操做,處理讀到的數據,註冊新的事件,而後返還控制權。

大體過程是,每一個應用程序宣佈它對某個socket感興趣,而後就須要到Reactor中註冊感興趣事件以及相關的處理函數。當socket發現有事件到達時,就會按順序對每一個事件進行處理(調用處理函數),當全部事件處理完成後,會繼續循環這整個操做。

過程以下圖所示:

從這個設計模式的處理過程當中能夠看出,多路IO複用模型就是使用的 Reactor模式,而且這種設計模式仍是體現的同步IO。

Proactor模式

Proactor的意思是主動器,主動去完成相應的工做不影響主流程。

Proactor模式的工做方式:

(1)應用程序初始化一個異步讀取操做,而後註冊相應的事件處理器,此時事件處理器不關注讀取就緒事件,而是關注讀取完成事件,這是區別於Reactor的關鍵。

(2)事件分離器等待讀取操做完成事件

(3)在事件分離器等待讀取操做完成的時候,操做系統調用內核線程完成讀取操做,並將讀取的內容放入用戶傳遞過來的緩存區中。這也是區別於Reactor的一點,Proactor中,應用程序須要傳遞緩存區。

(4)事件分離器捕獲到讀取完成事件後,激活應用程序註冊的事件處理器,事件處理器直接從緩存區讀取數據,而不須要進行實際的讀取操做。

異步IO模型就是使用的Proactor模式。





參考資料:

《Unix網絡編程》

www.cnblogs.com/dolphin0520…

www.cnblogs.com/findumars/p…

ifeve.com/io%E6%A8%A1…

相關文章
相關標籤/搜索