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