隨便談談 吃瓜和五種IO模型

五中io模型 

在《Unix網絡編程》一書中提到了五種IO模型,分別是:阻塞IO、非阻塞IO、多路複用IO、信號驅動IO以及異步IO。

 舉個吃瓜的例子

以下將我比作一個線程,我弟弟也是一個線程,冰箱爲一個socket,瓜爲數據,茶几爲緩衝區,我媽爲內核。

此時炎炎夏日,我正在打遊戲,口渴難耐。想吃西瓜,哈密瓜,香瓜

1.  阻塞io : 我先去家裏一臺冰箱裏找西瓜。西瓜有我就自己搬到茶几上切開吃,沒有我就站在冰箱面前等。等到我媽把瓜買回來放冰箱裏爲止。按照同樣步驟,吃完了西瓜,我再去下一個冰箱找哈密瓜,香瓜。

2.   非阻塞io : 我去家裏一臺冰箱裏找西瓜。瓜有我就自己搬到茶几上切開吃,沒有我就回來繼續打遊戲,死一次我就問一句我媽,冰箱裏有西瓜沒。 一直問到我媽發現冰箱裏真的有瓜了,並且告訴我。然後我就去自己把瓜搬到茶几上切開吃。 然後同樣步驟再去找哈密瓜、香瓜。

3.  io多路複用 :  我想吃瓜,讓我弟弟去看冰箱有沒有瓜,有就回來告訴我,沒有就繼續在冰箱面前等。 不管哪種瓜,只要弟弟說發現有瓜,我就立馬過去吃。吃完了繼續回來打遊戲。直到吃完三種瓜。

4. 信號驅動io : 我給冰箱裝上了一個感應器,一有瓜立馬發出報警聲,我和我媽說,冰箱響了告訴我。然後我就回去打遊戲了。 瓜買回來之後,冰箱真的響了,我媽做飯的時候聽到了,就告訴我說冰箱響了。我就出房門,把瓜從冰箱上搬到茶几上切開吃,吃完回來。再響一次我就又出房門去吃瓜。直到吃完。

5.  異步io :   我告訴我媽,幫我注意注意冰箱裏有沒有瓜,有瓜就幫我把瓜搬到茶几上順便幫我切好。 瓜來了我直接出房門吃就行了。  

 

下面就分別來介紹一下這5種IO模型的異同。

1.阻塞IO模型

  最傳統的一種IO模型,即在讀寫數據過程中會發生阻塞現象。

  當用戶線程發出IO請求之後,內核會去查看數據是否就緒,如果沒有就緒就會等待數據就緒,而用戶線程就會處於阻塞狀態,用戶線程交出CPU。當數據就緒之後,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用戶線程才解除block狀態。

典型的阻塞IO模型的例子爲:

data = socket.read();

如果數據沒有就緒,就會一直阻塞在read方法。

2.非阻塞IO模型

  當用戶線程發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦內核中的數據準備好了,並且又再次收到了用戶線程的請求,那麼它馬上就將數據拷貝到了用戶線程,然後返回。

  所以事實上,在非阻塞IO模型中,用戶線程需要不斷地詢問內核數據是否就緒,也就說非阻塞IO不會交出CPU,而會一直佔用CPU。

典型的非阻塞IO模型一般如下:

while(true){

   data = socket.read();

   if(data!= error){

       處理數據

       break;

   }

}

 

  但是對於非阻塞IO就有一個非常嚴重的問題,在while循環中需要不斷地去詢問內核數據是否就緒,這樣會導致CPU佔用率非常高,因此一般情況下很少使用while循環這種方式來讀取數據。

3.多路複用IO模型

  多路複用IO模型是目前使用得比較多的模型。

  在多路複用IO模型中,會有一個線程不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正調用實際的IO讀寫操作。因爲在多路複用IO模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必維護這些線程和進程,並且只有在真正有socket讀寫事件進行時,纔會使用IO資源,所以它大大減少了資源佔用。

  也許有朋友會說,我可以採用多線程+ 阻塞IO 達到類似的效果,但是由於在多線程 + 阻塞IO 中,每個socket對應一個線程,這樣會造成很大的資源佔用,並且尤其是對於長連接來說,線程的資源一直不會釋放,如果後面陸續有很多連接的話,就會造成性能上的瓶頸。

  而多路複用IO模式,通過一個線程就可以管理多個socket,只有當socket真正有讀寫事件發生纔會佔用資源來進行實際的讀寫操作。因此,多路複用IO比較適合連接數比較多的情況。

 

  另外多路複用IO爲何比非阻塞IO模型的效率高是因爲在非阻塞IO中,不斷地詢問socket狀態時通過用戶線程去進行的,而在多路複用IO中,輪詢每個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。

 

  不過要注意的是,多路複用IO模型是通過輪詢的方式來檢測是否有事件到達,並且對到達的事件逐一進行響應。因此對於多路複用IO模型來說,一旦事件響應體很大,那麼就會導致後續的事件遲遲得不到處理,並且會影響新的事件輪詢。

4.信號驅動IO模型

 

  在信號驅動IO模型中,當用戶線程發起一個IO請求操作,會給對應的socket註冊一個信號函數,然後用戶線程會繼續執行,當內核數據就緒時會發送一個信號給用戶線程,用戶線程接收到信號之後,便在信號函數中調用IO讀寫操作來進行實際的IO請求操作。這個一般用於UDP中,對TCP套接口幾乎是沒用的,原因是該信號產生得過於頻繁,並且該信號的出現並沒有告訴我們發生了什麼事情

5.異步IO模型

 

  異步IO模型纔是最理想的IO模型,在異步IO模型中,當用戶線程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從內核的角度,當它受到一個asynchronous read之後,它會立刻返回,說明read請求已經成功發起了,因此不會對用戶線程產生任何block。然後,內核會等待數據準備完成,然後將數據拷貝到用戶線程,當這一切都完成之後,內核會給用戶線程發送一個信號,告訴它read操作完成了。也就說用戶線程完全不需要關心實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收內核返回的成功信號時表示IO操作已經完成,可以直接去使用數據了。

 

  也就說在異步IO模型中,IO操作的兩個階段都不會阻塞用戶線程,這兩個階段都是由內核自動完成,然後發送一個信號告知用戶線程操作已完成。用戶線程中不需要再次調用IO函數進行具體的讀寫。這點是和信號驅動模型有所不同的,在信號驅動模型中,當用戶線程接收到信號表示數據已經就緒,然後需要用戶線程調用IO函數進行實際的讀寫操作;而在異步IO模型中,收到信號表示IO操作已經完成,不需要再在用戶線程中調用iO函數進行實際的讀寫操作。

 

  注意,異步IO是需要操作系統的底層支持,在Java 7中,提供了Asynchronous IO。簡稱AIO

 

前面四種IO模型實際上都屬於同步IO,只有最後一種是真正的異步IO,因爲無論是多路複用IO還是信號驅動模型,IO操作的第2個階段都會引起用戶線程阻塞,也就是內核進行數據拷貝的過程都會讓用戶線程阻塞。

兩種高性能IO設計模式

在傳統的網絡服務設計模式中,有兩種比較經典的模式:

  一種是多線程,一種是線程池。

  對於多線程模式,也就說來了client,服務器就會新建一個線程來處理該client的讀寫事件,如下圖所示:

這種模式雖然處理起來簡單方便,但是由於服務器爲每個client的連接都採用一個線程去處理,使得資源佔用非常大。因此,當連接數量達到上限時,再有用戶請求連接,直接會導致資源瓶頸,嚴重的可能會直接導致服務器崩潰。

  因此,爲了解決這種一個線程對應一個客戶端模式帶來的問題,提出了採用線程池的方式,也就說創建一個固定大小的線程池,來一個客戶端,就從線程池取一個空閒線程來處理,當客戶端處理完讀寫操作之後,就交出對線程的佔用。因此這樣就避免爲每一個客戶端都要創建線程帶來的資源浪費,使得線程可以重用。

  但是線程池也有它的弊端,如果連接大多是長連接,因此可能會導致在一段時間內,線程池中的線程都被佔用,那麼當再有用戶請求連接時,由於沒有可用的空閒線程來處理,就會導致客戶端連接失敗,從而影響用戶體驗。因此,線程池比較適合大量的短連接應用。

  因此便出現了下面的兩種高性能IO設計模式:Reactor和Proactor。

在Reactor模式中,會先對每個client註冊感興趣的事件,然後有一個線程專門去輪詢每個client是否有事件發生,當有事件發生時,便順序處理每個事件,當所有事件處理完之後,便再轉去繼續輪詢,如下圖所示:

從這裏可以看出,上面的五種IO模型中的多路複用IO就是採用Reactor模式。注意,上面的圖中展示的 是順序處理每個事件,當然爲了提高事件處理速度,可以通過多線程或者線程池的方式來處理事件。Java NIO使用的就是這種

  在Proactor模式中,當檢測到有事件發生時,會新起一個異步操作,然後交由內核線程去處理,當內核線程完成IO操作之後,發送一個通知告知操作已完成,可以得知,異步IO模型採用的就是Proactor模式。Java AIO使用的這種。

 

 

信號驅動和異步驅動的區別

  信號驅動IO是指:進程預先告知內核,使得 當某個socketfd有events(事件)發生時,內核使用信號通知相關進程。

  異步IO(Asynchronous IO)是指:進程執行IO系統調用(read / write)告知內核啓動某個IO操作,內核啓動IO操作後立即返回到進程。IO操作即內核當中的服務例程。

  異步I/O和信號驅動I/O的區別很容易被混淆。前者與後者的區別在於啓用異步I/O意味着通知內核啓動某個I/O操作,並讓內核在整個操作(包括數據從內核複製到用戶緩衝區)完成時通知我們。也就是說,異步I/O是由內核通知我們I/O操作何時完成,即實際的I/O操作也是異步的;而 信號驅動I/O是由內核通知我們何時可以啓動一個I/O。

  I/O究竟什麼時候能用這個信息實際上只有內核才能 事先知道,因爲是內核在最終處理系統中的所有打開的描述符。

  信號驅動I/O模型

  內核:I/O能用了。

  進程:接受到I/O能用的消息並執行接下來的操作。

   異步I/O模型

  內核:等待這個I/O有消息了,接受到數據。

  進程:從緩存中得到數據。

 

6. io多路複用實現方式

參考 https://blog.csdn.net/happy_wu/article/details/80052617

    按照實現時間先後,分別爲select/poll/epoll  。

   select  最早在BSD上實現,但是其有一些顯著缺點。

  • select 會修改傳入的參數數組,這個對於一個需要調用很多次的函數,是非常不友好的。
  • select 如果任何一個sock(I/O stream)出現了數據,select 僅僅會返回,但是並不會告訴你是那個sock上有數據。至於具體是哪個socket 還是需要線程再次去輪詢一遍。
  • select 默認32*機器 位數 的連接,在32位機器上只能監視1024個鏈接。
  • select 不是線程安全的

poll   對select 進行了改進,使用鏈表存連接參數,不再限制連接數量,除此之外本質上和select區別不大,提升有限。

epoll   則 是最新的多路複用實現,epoll 是線程安全的,而且epoll 不僅可以告訴你socket組裏有數據,還會直接告訴你是哪個socket。極大提升了 效率。不過epoll 只有Linux上實現了(BSD上對應爲kqueue)
 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章