IO模型
IO是Input/Output的縮寫。Linix網絡編程中有五種IO模型:
- blocking IO(阻塞IO)
- nonblocking IO(非阻塞IO)
- IO multiplexing(多路復用IO)
- signal driven IO(信號驅動IO)
- asynchronous IO(異步IO)
簡介
- Java.io包基於流模型實現,提供File抽象、輸入輸出流等IO的功能。交互方式是同步、阻塞的方式,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,線程會一直阻塞。java.io包的好處是代碼比較簡單、直觀,缺點則是IO效率和擴展性存在局限性,容易成為應用性能的瓶頸。
- Java.net下面提供的部分網絡API,比如Socket、ServerSocket、HttpURLConnection 也時常被歸類到同步阻塞IO類庫,網絡通信同樣是IO行為
- Java 1.4中引入了NIO框架(java.nio 包),提供了Channel、Selector、Buffer等新的抽象,可以構建多路復用IO程序,同時提供更接近操作系統底層的高性能數據操作方式。
- Java7中,NIO有了進一步的改進,也就是NIO2,引入了異步非阻塞IO方式,也被稱為AIO(Asynchronous IO),異步IO操作基於事件和回調機制。
首先了解下同步\異步、阻塞\非阻塞的區別
同步與異步
同步和異步是針對的是用戶進程與內核的交互方式。
- 同步指的是用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒。例如:自己去銀行辦理業務,自己只能一直干這件事,其他事情只能等這件是做完后再做
- 異步指的是用戶進程觸發IO操作以后便開始做其他的事情,而當IO操作已經完成的時候會得到IO完成的通知。例如:委托親屬去銀行辦理業務,然后自己可以去干別的事。(使用異步I/O時,Java將I/O讀寫委托給OS處理,需要將數據緩沖區地址和大小傳給OS)。
阻塞與非阻塞
阻塞和非阻塞是針對進程在訪問數據的時候,根據IO操作的就緒狀態來采取的不同方式。
- 阻塞指的是當試圖對該文件描述符進行讀寫時,如果當時沒有東西可讀,或暫時不可寫,程序就進入等待狀態,直到有東西可讀或可寫為止。去辦理業務時,人過多需要排隊,此時就在原地等待,一直等到自己為止。
- 非阻塞指的是如果沒有東西可讀,或不可寫,讀寫函數馬上返回,而不會等待。在銀行里辦業務時,領取一張小票,之后我們可以玩手機,或與別人聊聊天,當輪到我們時,銀行的喇叭會通知,這時候我們就可以去辦業務了。
注意,這里辦業務的時候,還是需要我們也參與其中的,這和異步是完全不同的,因此同步\異步、阻塞\非阻塞,是完全不同的兩個概念,二者不要混淆
I/O模型分類
應用程序向操作系統發出IO請求:應用程序發出IO請求給操作系統內核,操作系統內核需要等待數據就緒,這里的數據可能來自別的應用程序或者網絡。一般來說,一個IO分為兩個階段:
- 等待數據:數據可能來自其他應用程序或者網絡,如果沒有數據,應用程序就阻塞等待。
- 拷貝數據:將就緒的數據拷貝到應用程序工作區。
在Linux系統中,操作系統的IO操作是一個系統調用recvfrom(),即一個系統調用recvfrom包含兩步,等待數據就緒和拷貝數據。
同步阻塞IO
在此種方式下,用戶進程在發起一個IO操作以后,必須等待IO操作的完成,只有當IO操作完成之后,用戶進程才能運行。JAVA傳統的BIO屬於此種方式。(jdk1.4以前)
同步非阻塞IO
JAVA NIO(jdk1.4以后引入)
在此種方式下,用戶進程發起一個IO操作以后邊可返回做其它事情,但是用戶進程需要時不時的詢問IO操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的CPU資源浪費。JAVA的NIO就屬於同步非阻塞IO
多路復用IO
redis、nginx、netty;reactor模式
select,epoll;有時也稱這種IO方式為事件驅動IO。
select/epoll的好處就在於單個process就可以同時處理多個網絡連接的IO。它的基本原理就是select/epoll這個函數會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程.
多路復用中,通過select函數,可以同時監聽多個IO請求的內核操作,只要有任意一個IO的內核操作就緒,都可以通知select函數返回,再進行系統調用recvfrom()完成IO操作。
這個過程應用程序就可以同時監聽多個IO請求,這比起基於多線程阻塞式IO要先進得多,因為服務器只需要少數線程就可以進行大量的客戶端通信。
信號驅動式IO模型
在unix系統中,應用程序發起IO請求時,可以給IO請求注冊一個信號函數,請求立即返回,操作系統底層則處於等待狀態(等待數據就緒),直到數據就緒,然后通過信號通知主調程序,主調程序才去調用系統函數recvfrom()完成IO操作。
信號驅動也是一種非阻塞式的IO模型,比起上面的非阻塞式IO模型,信號驅動式IO模型不需要輪詢檢查底層IO數據是否就緒,而是被動接收信號,然后再調用recvfrom執行IO操作。
比起多路復用IO模型來說,信號驅動IO模型針對的是一個IO的完成過程, 而多路復用IO模型針對的是多個IO同時進行時候的場景。
異步IO
在此種模式下,整個IO操作(包括等待數據就緒,復制數據到應用程序工作空間)全都交給操作系統完成。數據就緒后操作系統將數據拷貝進應用程序運行空間之后,操作系統再通知應用程序,這個過程中應用程序不需要阻塞
區別
如果你在燒水:
- 同步阻塞:你將水放在爐子上,然后在那兒等着,還要一直觀察:水燒開了沒啊!
- 同步非阻塞:你將水放在爐子上,就去看電視了了。每過一會,就到爐子邊觀察:水燒開了沒啊!
- 多路復用:有人改進了燒水壺,水開了之后會自動發出哨聲,你只需要安心看電視等待哨響通知你水燒開了。
- 異步非阻塞:你安排其他人燒水,水燒開后放在特地場合,會打電話通知你,安心看電視等待就可以了。
阻塞、非阻塞、多路IO復用,都是同步IO,異步必定是非阻塞的,所以不存在異步阻塞和異步非阻塞的說法。真正的異步IO需要CPU的深度參與。換句話說,只有用戶線程在操作IO的時候根本不去考慮IO的執行,全部都交給CPU去完成,而只需要等待一個完成信號的時候,才是真正的異步IO。所以,fork子線程去輪詢、死循環或者使用select、poll、epoll,都不是異步
比較經典的一個舉例
-
阻塞I/O模型
老李去火車站買票,排隊三天買到一張退票。 耗費:在車站吃喝拉撒睡 3天,其他事一件沒干。
-
非阻塞I/O模型
老李去火車站買票,隔12小時去火車站問有沒有退票,三天后買到一張票。耗費:往返車站6次,路上6小時,其他時間做了好多事。
-
I/O復用模型
1.select/poll 老李、老王、老劉…一行人去火車站買票,一起委托給黃牛(select黃牛最大只能接1024個人的訂單/pool黃牛不限制),select/pool黃牛一直等待出票結果,待黃牛取到票后,不知道這張票是屬於誰的(需要根據票面逐一詢問),確認后通知相應人去火車站交錢領票。
2.epoll 老李、老王、老劉…一行人(無人員個數限制)去火車站買票,一起委托給黃牛,黃牛買到后不需要確認就可以知道這張票的委托人是誰,然后通知其去火車站交錢領票。
多路復用的意思是:黃牛在承接老李的訂單之后,同時也接了老王、老劉的購票訂單;大家使用同一個黃牛
-
信號驅動I/O模型
老李去火車站買票,給售票員留下電話,有票后,售票員電話通知老李,然后老李去火車站交錢領票。 耗費:往返車站2次,路上2小時,免黃牛費100元,無需打電話,也不需要黃牛
-
異步I/O模型
老李去火車站買票,給售票員留下電話,有票后,售票員快遞送票上門后電話通知其收貨。 耗費:往返車站1次,路上1小時,免黃牛費100元,無需打電話,也不需要黃牛
再談IO多路復用
I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
多路復用如下面所示:指的其實是在單個線程通過記錄跟蹤每一個Sock(I/O流)的狀態來同時管理多個I/O流
個人的一些理解:
如上圖做一個簡單的比喻:左邊有若干取水器,需要到右邊水龍頭進行取水操作,每個取水器和水龍頭是一一對應的關系,但是中間段是斷開的,需要將水管連接上(一個水管相當於一個IO線程),才可以進行取水操作了(注意水龍頭不是一直都有水流的,只有當取水器連接上才會觸發輸水操作)。下面依次對不同的IO模型進行講解:
1、傳統阻塞BIO:每個取水器和水龍頭之間都需要一個連接水管,水管連接上觸發取水操作,水龍頭才會輸水。這樣有幾個取水器就需要幾個水管,另外水管接上之后並不會馬上就能取到水,之間一直處於阻塞狀態,當取水器過多時沒有足夠的水管來進行連接
線程池模式:水池中存在10根水管,每當取水器有取水請求時,就去水池中拿一根水管使用,水管會根據取水器編號接到相應的水龍頭上。當取水器請求過多時,需要不停的進行水管切換。
2、多路復用IO:
select/poll:全部的取水器均復用一根水管,沒有多余的水管可用,所有的取水器均接到這一根水管上。(區別是select模式僅支持1024個取水器的接入,而poll不限制取水器個數)。當水龍頭有水流過來時,水管會提前收到通知,但不知道是哪個水龍頭。則此時水管需要每個水龍頭都接上試一下,當發現其中一個水龍頭有水流時則將其運到與其相連的取水器中。
epoll部的取水器均復用一根水管,沒有多余的水管可用,所有的取水器均接到這一根水管上。(與select/poll區別是:當水龍頭有水流過來時,水管就已經知道是哪根水龍頭在運水了,直接將水管接上相應的水龍頭即可)。
偽代碼描述各IO區別
-
非阻塞忙輪詢式
while true { for i in fd[] { if i has data read until unavailable } }
把所有流從頭到尾查詢一遍,就可以處理多個流了,但這樣做很不好,因為如果所有的流都沒有I/O事件,白白浪費CPU時間片
-
select:服務端一直在輪詢、監聽如果有客戶端鏈接上來就創建一個連接放到數組A中,繼續輪詢這個數組,如果在輪詢的過程中有客戶端發生IO事件就去處理;select只能監視1024個連接(一個進程只能創建1024個文件);而且存在線程安全問題;
while true { select(fds[]) //阻塞這里,直到有一個流有I/O事件時,才往下執行,數組的大小只有1024 for i in fds[] { if i has data read until unavailable } }
它僅僅知道了,有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長
-
poll:在select做了許多修復,比如不限制監測的連接數;但是也有線程安全問題;
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的.
-
epoll:也是監測IO事件,但是如果發生IO事件,它會告訴你是哪個連接發生了事件,就不用再輪詢訪問。而且它是線程安全的,但是只有linux平台支持;
while true { active_fds[] = epoll_wait(epollfd) for i in active_fds[] { read or write till } }
epoll可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))