想理解Java的IO,不要從操作系統開始說起的都是耍流氓

Java網絡IO涵蓋的知識體系很廣泛,本文將簡單介紹Java網絡IO的相關知識

從操作系統開始

爲了保護操作系統的安全,會將內存分爲用戶空間和內核空間兩個部分。如果用戶想要操作內核空間的數據,則需要把數據從內核空間拷貝到用戶空間

舉個栗子,如果服務器收到了從客戶端過來的請求,並且想要進行處理,那麼需要經過這幾個步驟:

  • 服務器的網絡驅動接收到消息之後,向內核申請空間,並在收到完整的數據包(這個過程會產生延時,因爲有可能是通過分組傳送過來的)後,將其複製到內核空間;
  • 數據從內核空間拷貝到用戶空間;
  • 用戶程序進行處理。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

因此我們可以將服務器接收消息理解爲兩個階段:

  • 等待數據到達
  • 將數據從內核空間拷貝到用戶空間

在操作系統中的IO

在此以Linux操作系統爲例。Linux是一個將所有的外部設備都看作是文件來操作的操作系統,在它看來:everything is a file,那麼我們就把對於外部設備的操作都看作是對文件進行操作。而且我們對一個文件進行讀寫,都需要通過調用內核提供的系統調用。

而在Linux中,一個基本的IO會涉及到兩個系統對象:一個是調用這個IO的進程對象(用戶進程),另一個是系統內核。也就是說,當一個read操作發生時,將會經歷這些階段:

  • 通過read系統調用,向內核發送讀請求;
  • 內核向硬件發送讀指令,並等待讀就緒;
  • DMA把將要讀取的數據複製到指定的內核緩存區中;
  • 內核將數據從內核緩存區拷貝到用戶進程空間中

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

在此期間會發生幾種IO操作:

  • 同步IO:當用戶發出IO請求操作後,內核會去查看要讀取的數據是否就緒,如果沒有,就一直等待。期間用戶線程或內存會不斷地輪詢數據是否就緒。當數據就緒時,再把數據從內核拷貝到用戶空間。
  • 異步IO:用戶線程只需發出IO請求和接收IO操作完成通知,期間的IO操作由內核自動完成,併發送通知告知用戶線程IO操作已經完成。也就是說,在異步IO中,並不會對用戶線程產生任何阻塞。
  • 阻塞IO:當用戶線程發起一個IO請求操作,而內核要操作的數據還沒就緒,則當前線程被掛起,阻塞等待結果返回。
  • 非阻塞IO:如果數據沒有就緒,就會返回一個標誌信息告知用戶線程,當前的數據還沒有就緒。當前線程在獲得此次請求結果的過程中,還可以做點其他事情。

可能會有讀者覺得,怎麼同步IO、異步IO和阻塞IO、非阻塞IO的操作好相似,爲什麼要它們都分出來呢?筆者認爲,這同步、異步和阻塞、非阻塞是從不同角度來看待問題的

同步與異步

同步與異步主要是從消息通知的角度來說的。

同步就是當一個任務A的完成需要依賴另一個任務B時,只有等到B任務完成後,A才能成功地進行,這是一種可靠的任務隊列。要麼都成功,要麼都失敗,兩個任務的狀態可以保持一致。

異步是不需要等待任務B完成,只是通知任務B要完成什麼工作,任務A也立即執行,只要任務A自己執行完了那麼整個任務就算完成了。至於任務B最終是否真正完成,A任務無法確定,所以這是不可靠的一種任務隊列

舉個栗子,假如小J要去銀行櫃檯辦事,拿號排隊。如果他只盯着號碼提示牌,還時不時問是否到他了,這就是同步;如果他拿了號之後就去打電話了,等到排到他的時候櫃員通知他去辦理業務,這就是異步。他們之間的區別就在於,等待消息通知的方式不同。

阻塞與非阻塞

阻塞與非阻塞主要是從等待消息通知時的狀態角度來說的。

阻塞就是指在調用結果返回之前,當前線程會被掛起,一直處於等待消息通知的狀態,不能執行其他業務。只有當調用結果返回之後才能進行其他操作。

非阻塞與阻塞的概念相對應,就是指不能立即得到結果之前,該函數不會阻塞當前線程,而是會立即返回。雖然非阻塞的方式看上去可以明顯提高CPU的利用率,但是也會使系統的線程切換增加,需要好好評估增加的CPU執行時間能不能步長系統的切換成本。

我們繼續用上面的栗子,小J無論是在排隊還是拿號等通知,如果在這個等待的過程中,小J除了等待消息通知之外就做不了其他的事情,那麼該機制就是阻塞的。如果他可以一邊打電話一邊等待,這個狀態就是非阻塞的。

同步、異步與阻塞、非阻塞

其實可能會有其他讀者把同步與阻塞等同起來,實際上這兩個是不同的。對於同步來說,很多時候當前線程還是在激活狀態,只是邏輯上當前函數沒有返回而已,此時,線程也會去處理其他的消息。也就是說,同步、阻塞其實是在消息通知機制下從不同角度對當前線程狀態的描述

5.1 同步阻塞形式

這是效率最低的一種方式,拿上面的栗子來說,就是小J心無旁騖地排隊,什麼別的事都不做

在這裏,同步與阻塞體現在:

  • 同步:小J等待隊伍排到他辦理業務;
  • 阻塞:小J在等待隊伍排到他的過程中,不做其他任務處理。

5.2 異步阻塞形式

如果小J在銀行等待辦理業務的時候,領了號,這時候就採用了異步的方式去等待消息被觸發(通知),等着櫃員喊他的號而不是時刻盯着是不是排到他了。但是在這段時間裏,他還是不能離開銀行去做其他的事情,那麼很顯然,他被阻塞在這個等待喊號的操作上了。

在這裏,異步與阻塞體現在:

  • 異步:排到小J的話櫃員會喊他的號碼;
  • 阻塞:等待喊號的過程中,不能做其他事情。

5.3 同步非阻塞形式

實際上效率也是低下。小J在排隊的過程中可以打電話,但是要邊打電話邊看看還有多久才排到他。如果將打電話和觀察排隊情況看成是程序中的兩個操作的話,這個程序需要在這兩個不同的行爲之間來回切換。

在這裏,同步與非阻塞體現在:

  • 同步:排隊等待輪到他辦理業務;
  • 非阻塞:可以在排隊的過程中打電話,只不過要時不時看看還要多久才排到他辦理業務。

5.4 異步非阻塞形式

這是一個效率更高的模式。小J在拿號之後可以去打電話,只要等待櫃員喊號就可以了,在這裏打電話是等待着的事情,而通知小J辦理業務是櫃員的事情。

在這裏,異步和非阻塞體現在:

  • 異步:櫃員喊小J去辦理業務;
  • 非阻塞:在等待喊號的過程中,小J去打電話,只要接收到櫃員喊號的通知即可,無需關注是否隊伍的進度。

也就是說,同步和異步僅需關注消息如何通知的機制,而阻塞和非阻塞關注的是在等待消息通知的過程中能不能去做別的事。在同步情況下,是由處理者自己去等待消息是否被觸發,而異步情況下是由觸發機制來通知處理者處理業務。

Linux的五種IO模型

在我們瞭解Linux操作系統的IO操作,以及同步與異步、阻塞與非阻塞的概念之後,我們來看看Linux系統中根據同步、異步、阻塞、非阻塞實現的五種IO模型。以Linux下的系統調用recv爲例,是一個用於從套接字上接收一個消息,因爲是系統調用,所以在調用的時候,會從用戶空間切換到內核空間運行一段時間後,再切換回來。在默認情況下recv會等到網絡數據到達並複製到用戶空間或發生錯誤時返回。

6.1 同步阻塞IO模型

從系統調用recv到將數據從內核複製到用戶空間並返回,在這段時間內進程始終阻塞。就相當於,小J想去櫃檯辦理業務,如果櫃檯業務繁忙,他也要排隊,直到排到他辦理完業務,才能去做別的事。顯然,這個IO模型是同步且阻塞的。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

6.2 同步非阻塞IO模型

在這裏recv不管有沒有獲得到數據都返回,如果沒有數據的話就過段時間再調用recv看看,如此循環。就像是小J來櫃檯辦理業務,發現櫃員休息,他離開了,過一會又過來看看營業了沒,直到終於碰到櫃員營業了,這才辦理了業務。而小J在中間離開的時間,可以做他自己的事情。但是這個模型只有在檢查無數據的時候是非阻塞的,在數據到達的時候依然要等待複製數據到用戶空間(辦理業務),因此它還是同步IO。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

6.3 IO複用模型

在IO複用模型中,調用recv之前會先調用select或poll,這兩個系統調用都可以在內核準備好數據(網絡數據已經到達內核了)時告知用戶進程,它準備好了,這時候再調用recv時是一定有數據的。因此在這一模型中,進程阻塞於select或poll,而沒有阻塞在recv上。就相當於,小J來銀行辦理業務,大堂經理告訴他現在所有櫃檯都有人在辦理業務,等有空位再告訴他。於是小J就等啊等(select或poll調用中),過了一會兒大堂經理告訴他有櫃檯空出來可以辦理業務了,但是具體是幾號櫃檯,你自己找下吧,於是小J就只能挨個櫃檯地找。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

6.4 信號驅動IO模型

此處會通過調用sigaction註冊信號函數,在內核數據準備好的時候系統就中斷當前程序,執行信號函數(在這裏調用recv)。相當於,小J讓大堂經理在櫃檯有空位的時候通知他(註冊信號函數),等沒多久大堂經理通知他,因爲他是銀行的VIPPP會員,所以專門給他開了一個櫃檯來辦理業務,小J就去特席櫃檯辦理業務了。但即使在等待的過程中是非阻塞的,但在辦理業務的過程中依然是同步的。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

6.5 異步IO模型

調用aio_read令內核把數據準備好,並且複製到用戶進程空間後執行事先指定好的函數。就像是,小J交代大堂經理把業務給辦理好了就通知他來驗收,在這個過程中小J可以去做自己的事情。這就是真正的異步IO。

想理解Java的IO,不要從操作系統開始說起的都是耍流氓

 

我們可以看到,前四種模型都是屬於同步IO,因爲在內核數據複製到用戶空間的這一過程都是阻塞的。而最後一種異步IO,通過將IO操作交給操作系統處理,當前進程不關心具體IO的實現,後來再通過回調函數,或信號量通知當前進程直接對IO返回結果進行處理。

BIO、NIO、AIO的區別

上文談到IO的四種模式:同步阻塞IO、同步非阻塞IO、異步阻塞IO、異步非阻塞IO,在JavaIO中提供了三種模式的實現:BIO(同步阻塞IO)、NIO(同步非阻塞IO)、AIO(異步非阻塞IO)。至於這四種模式之間的區別,上文已經有較爲詳細的介紹了,接下來筆者將對這三種JavaIO類型之間的區別進行介紹。

  • BIO:同步並阻塞,在服務器中實現的模式爲一個連接一個線程。也就是說,客戶端有連接請求的時候,服務器就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然這也可以通過線程池機制改善。BIO一般適用於連接數目小且固定的架構,這種方式對於服務器資源要求比較高,而且併發侷限於應用中,是JDK1.4之前的唯一選擇,但好在程序直觀簡單,易理解。
  • NIO:同步並非阻塞,在服務器中實現的模式爲一個請求一個線程,也就是說,客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到有連接IO請求時纔會啓動一個線程進行處理。NIO一般適用於連接數目多且連接比較短(輕操作)的架構,併發侷限於應用中,編程比較複雜,從JDK1.4開始支持。
  • AIO:異步並非阻塞,在服務器中實現的模式爲一個有效請求一個線程,也就是說,客戶端的IO請求都是通過操作系統先完成之後,再通知服務器應用去啓動線程進行處理。AIO一般適用於連接數目多且連接比較長(重操作)的架構,充分調用操作系統參與併發操作,編程比較複雜,從JDK1.7開始支持。

結語

本文從操作系統進行文件讀寫入手,對同步、異步、阻塞、非阻塞以及它們組合而成的IO模式進行了介紹,還了解Linux操作系統中的五種IO模型,以及重新回到JavaIO,看待BIO、NIO、AIO之間的區別。

如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~

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