阻塞I/O(blocking I/O)模型,進程調用recvfrom,其系統調用直到數據報到達且被拷貝到應用進程的緩衝區中或者發生錯誤才返回。進程從調用recvfrom開始到它返回的整段時間內是被阻塞的。java
當一個應用進程像這樣對一個非阻塞描述字循環調用recvfrom時,咱們稱之爲輪詢(polling)。應用進程持續輪詢內核,以查看某個操做是否就緒。react
根據上述5種IO模型,前4種模型-阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步I/O模型,由於其中真正的I/O操做(recvfrom)將阻塞進程,在內核數據copy到用戶空間時都是阻塞的。linux
一個IO操做能夠分爲兩個步驟:發起IO請求和實際的IO操做
例如:
一、操做系統的一次寫操做分爲兩步:將數據從用戶空間拷貝到系統空間;從系統空間往網卡寫。
二、一次讀操做分爲兩步:將數據從網卡拷貝到系統空間;將數據從系統空間拷貝到用戶空間。編程
阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,若是阻塞直到完成那麼就是傳統的阻塞IO,若是不阻塞,那麼就是非阻塞IO。windows
同步IO和異步IO的區別就在於第二個步驟是否阻塞,若是實際的IO讀寫阻塞請求進程,那麼就是同步IO,所以阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO,若是不阻塞,而是操做系統作完IO兩個階段的操做再將結果返回,那麼就是異步IO。緩存
IO多路複用,就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。性能優化
從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操做,效率更差。可是,使用select之後最大的優點是用戶能夠在一個線程內同時處理多個socket的IO請求。用戶能夠註冊多個socket,而後不斷地調用select讀取被激活的socket,便可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須經過多線程的方式才能達到這個目的。服務器
IO多路複用方式容許單線程內處理多個IO請求,可是每一個IO請求的過程仍是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。若是用戶線程只註冊本身感興趣的socket或者IO請求,而後去作本身的事情,等到數據到來時再進行處理,則能夠提升CPU的利用率。
因爲select函數是阻塞的,所以多路IO複用模型也被稱爲異步阻塞IO模型。注意,這裏的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。通常在使用IO多路複用模型時,socket都是設置爲NONBLOCK的,不過這並不會產生影響,由於用戶發起IO請求時,數據已經到達了,用戶線程必定不會被阻塞。
IO多路複用是最常使用的IO模型,可是其異步程度還不夠「完全」,由於它使用了會阻塞線程的select系統調用。所以IO多路複用只能稱爲異步阻塞IO,而非真正的異步IO。網絡
展現了非阻塞IO如何讓你使用一個selector區處理多個鏈接.多線程
Linux支持IO多路複用的系統調用有select、poll、epoll,這些調用都是內核級別的。但select、poll、epoll本質上都是同步I/O,先是block住等待就緒的socket,再是block住將數據從內核拷貝到用戶內存。
在這兩種模式下的事件多路分離器反饋給程序的信息是不同的:
1.Reactor模式下說明你能夠進行讀寫(收發)操做了。
2.Proactor模式下說明已經完成讀寫(收發)操做了,具體內容在給定緩衝區中,能夠對這些內容進行其餘操做了。
Reactor關注的是I/O操做的就緒事件,而Proactor關注的是I/O操做的完成事件
通常地,I/O多路複用機制都依賴於一個事件多路分離器(Event Demultiplexer)。分離器對象可未來自事件源的I/O事件分離出來,並分發到對應的read/write事件處理器(Event Handler)。
Reactor模式採用同步IO,而Proactor採用異步IO。
在Reactor中,事件分離器負責等待文件描述符或socket爲讀寫操做準備就緒,而後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工做。
而在Proactor模式中,處理器或者兼任處理器的事件分離器,只負責發起異步讀寫操做。IO操做自己由操做系統來完成。傳遞給操做系統的參數須要包括用戶定義的數據緩衝區地址和數據大小,操做系統才能從中獲得寫出操做所需數據,或寫入從socket讀到的數據。事件分離器捕獲IO操做完成事件,而後將事件傳遞給對應處理器。好比,在windows上,處理器發起一個異步IO操做,再由事件分離器等待IOCompletion事件。典型的異步模式實現,都創建在操做系統支持異步API的基礎之上,咱們將這種實現稱爲「系統級」異步或「真」異步,由於應用程序徹底依賴操做系統執行真正的IO工做。
Reactor和Proactor模式的主要區別就是真正的讀取和寫入操做是有誰來完成的,Reactor中須要應用程序本身讀取或者寫入數據,而Proactor模式中,應用程序不須要進行實際的讀寫過程,它只須要從緩存區讀取或者寫入便可,操做系統會讀取緩存區或者寫入緩存區到真正的IO設備.
NIO,有人稱之爲New I/O,由於它相對於以前的I/O類庫是新增的,因此被稱爲New I/O。可是,因爲以前老的 I/O 類庫是阻塞 I/O,New I/O類庫的目標就是要讓Java支持非阻塞 I/O,因此,更多的人喜歡稱之爲非阻塞 I/ O(Non-block I/O)。
注意,select是阻塞的,不管是經過操做系統的通知(epoll)仍是不停的輪詢(select,poll),這個函數是阻塞的。因此你能夠放心大膽地在一個while(true)裏面調用這個函數而不用擔憂CPU空轉。
NIO採用Reactor模式,一個Reactor線程聚合一個多路複用器Selector,它能夠同時註冊、監聽和輪詢成百上千個Channel,一個IO線程能夠同時併發處理N個客戶端鏈接,線程模型優化爲1:N(N < 進程可用的最大句柄數)或者M : N (M一般爲CPU核數 + 1, N < 進程可用的最大句柄數)。
JAVA NIO 不是同步非阻塞I/O嗎,爲何說JAVA NIO提供了基於Selector的異步網絡I/O?
java nio的io模型是同步非阻塞,這裏的同步異步指的是真正io操做(數據內核態用戶態的拷貝)是否須要進程參與。
而說java nio提供了異步處理,這個異步應該是指編程模型上的異步。基於reactor模式的事件驅動,事件處理器的註冊和處理器的執行是異步的。
AIO(Async I/O)裏面會更進一步:不但等待就緒是非阻塞的,就連數據從網卡到內存的過程也是異步的。
換句話說,BIO裏用戶最關心「我要讀」,NIO裏用戶最關心"我能夠讀了",在AIO模型裏用戶更須要關注的是「讀完了」。
NIO一個重要的特色是:socket主要的讀、寫、註冊和接收函數,在等待就緒階段都是非阻塞的,真正的I/O操做是同步的(消耗CPU但性能很是高)。
BIO模型,之因此須要多線程,是由於在進行I/O操做的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能"傻等",即便經過各類估算,算出來操做系統沒有能力進行讀寫,也無法在socket.read()和socket.write()函數中返回,這兩個函數沒法進行有效的中斷。因此除了多開線程另起爐竈,沒有好的辦法利用CPU。
NIO的讀寫函數能夠馬上返回,這就給了咱們不開線程利用CPU的最好機會:若是一個鏈接不能讀寫(socket.read()返回0或者socket.write()返回0),咱們能夠把這件事記下來,記錄的方式一般是在Selector上註冊標記位,而後切換到其它就緒的鏈接(channel)繼續進行讀寫。
咱們大概能夠總結出NIO是怎麼解決掉線程的瓶頸並處理海量鏈接的:
NIO由原來的阻塞讀寫(佔用線程)變成了單線程輪詢事件,找到能夠進行讀寫的網絡描述符進行讀寫。除了事件的輪詢是阻塞的(沒有可乾的事情必需要阻塞),剩餘的I/O操做都是純CPU操做,沒有必要開啓多線程。
而且因爲線程的節約,鏈接數大的時候由於線程切換帶來的問題也隨之解決,進而爲處理海量鏈接提供了可能。
不少人喜歡將JDK1.4提供的NIO框架稱爲異步非阻塞I/O,可是,若是嚴格按照UNIX網絡編程模型和JDK的實現進行區分,實際上它只能被稱爲非阻塞I/O,不能叫異步非阻塞I/O。在早期的JDK1.4和1.5 update10版本以前,JDK的Selector基於select/poll模型實現,它是基於I/O複用技術的非阻塞I/O,不是異步I/O。在JDK1.5 update10和Linux core2.6以上版本,Sun優化了Selctor的實現,它在底層使用epoll替換了select/poll,上層的API並無變化,能夠認爲是JDK NIO的一次性能優化,可是它仍舊沒有改變I/O的模型。
由JDK1.7提供的NIO2.0,新增了異步的套接字通道,它是真正的異步I/O,在異步I/O操做的時候能夠傳遞信號變量,當操做完成以後會回調相關的方法,異步I/O也被稱爲AIO。
NIO類庫支持非阻塞讀和寫操做,相比於以前的同步阻塞讀和寫,它是異步的,所以不少人習慣於稱NIO爲異步非阻塞I/O,包括不少介紹NIO編程的書籍也沿用了這個說法。爲了符合你們的習慣,咱們也將NIO稱爲異步非阻塞I/O或者非阻塞I/O。
基本上,全部的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據能夠從Channel讀到Buffer中,也能夠從Buffer 寫到Channel中。這裏有個圖示:
Selector容許單線程處理多個Channel。若是你的應用打開了多個鏈接(通道),但每一個鏈接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。
這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector,得向Selector註冊Channel,而後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子有如新鏈接進來,數據接收等。
最後總結一下到底NIO給咱們帶來了些什麼:
事件驅動模型
避免多線程
單線程處理多任務
非阻塞I/O,I/O讀寫再也不阻塞,而是返回0
基於block的傳輸,一般比基於流的傳輸更高效
更高級的IO函數,zero-copy
IO多路複用大大提升了Java網絡應用的可伸縮性和實用性
BIO | NIO | AIO 以Java的角度,理解以下:
在JDK1.4以前,用Java編寫網絡請求,都是創建一個ServerSocket,而後,客戶端創建Socket時就會詢問是否有線程能夠處理,若是沒有,要麼等待,要麼被拒絕。即:一個鏈接,要求Server對應一個處理線程。
在Java裏的由來,在JDK1.4及之後版本中提供了一套API來專門操做非阻塞I/O,咱們能夠在java.nio包及其子包中找到相關的類和接口。因爲這套API是JDK新提供的I/O API,所以,也叫New I/O,這就是包名nio的由來。這套API由三個主要的部分組成:緩衝區(Buffers)、通道(Channels)和非阻塞I/O的核心類組成。在理解NIO的時候,須要區分,說的是New I/O仍是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。這裏講的是後面一種。
NIO自己是基於事件驅動思想來完成的,其主要想解決的是BIO的大併發問題: 在使用同步I/O的網絡應用中,若是要同時處理多個客戶端請求,或是在客戶端要同時和多個服務器進行通信,就必須使用多線程來處理。也就是說,將每個客戶端請求分配給一個線程來單獨處理。這樣作雖然能夠達到咱們的要求,但同時又會帶來另一個問題。因爲每建立一個線程,就要爲這個線程分配必定的內存空間(也叫工做存儲器),並且操做系統自己也對線程的總數有必定的限制。若是客戶端的請求過多,服務端程序可能會由於不堪重負而拒絕客戶端的請求,甚至服務器可能會所以而癱瘓。
NIO基於Reactor,當socket有流可讀或可寫入socket時,操做系統會相應的通知引用程序進行處理,應用再將流讀取到緩衝區或寫入操做系統。
也就是說,這個時候,已經不是一個鏈接就要對應一個處理線程了,而是有效的請求,對應一個線程,當鏈接沒有數據時,是沒有工做線程來處理的。
與NIO不一樣,操做系統負責處理內核區/用戶區的內存數據遷移和真正的IO操做,應用程序只須直接調用API的read或write方法便可。這兩種方法均爲異步的,對於讀操做而言,當有流可讀取時,操做系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操做而言,當操做系統將write方法傳遞的流寫入完畢時,操做系統主動通知應用程序。
便可以理解爲,read/write方法都是異步的,完成後會主動調用回調函數。
在JDK1.7中,這部份內容被稱做NIO.2,主要在java.nio.channels包下增長了下面四個異步通道:
其中的read/write方法,會返回一個帶回調函數的對象,當執行完讀取/寫入操做後,直接調用回調函數。
說道實現原理,還要從操做系統的IO模型上了解
按照《Unix網絡編程》的劃分,IO模型能夠分爲:阻塞IO、非阻塞IO、IO複用、信號驅動IO和異步IO,按照POSIX標準來劃分只分爲兩類:同步IO和異步IO。如何區分呢?首先一個IO操做其實分紅了兩個步驟:發起IO請求和實際的IO操做,同步IO和異步IO的區別就在於第二個步驟是否阻塞,若是實際的IO讀寫阻塞請求進程,那麼就是同步IO,所以阻塞IO、非阻塞IO、IO複用、信號驅動IO都是同步IO,若是不阻塞,而是操做系統幫你作完IO操做再將結果返回給你,那麼就是異步IO。阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,若是阻塞直到完成那麼就是傳統的阻塞IO,若是不阻塞,那麼就是非阻塞IO。
能夠理解的說明是:在Linux 2.6之後,java NIO的實現,是經過epoll來實現的,這點能夠經過jdk的源代碼發現。而AIO,在windows上是經過IOCP實現的,在linux上經過新的API來實現。