【死磕NIO】— 阻塞IO,非阻塞IO,IO複用,訊號驅動IO,非同步IO,這你真的分的清楚嗎?

chenssy發表於2021-10-08

通過上篇文章(【死磕NIO】— 阻塞、非阻塞、同步、非同步,傻傻分不清楚),我想你應該能夠區分了什麼是阻塞、非阻塞、非同步、非非同步了,這篇文章我們來徹底弄清楚什麼是阻塞IO,非阻塞IO,IO複用,訊號驅動IO,非同步IO。

要想徹底弄清楚這五種IO模型,我們需要先弄清楚幾個基本概念。

基本概念

什麼是IO

什麼是IO?維基百科上面是這樣解釋的:

I/O(英語:Input/Output),即輸入/輸出,通常指資料在儲存器(內部和外部)或其他周邊裝置之間的輸入和輸出,是資訊處理系統(例如計算機)與外部世界(可能是人類或另一資訊處理系統)之間的通訊。輸入是系統接收的訊號或資料,輸出則是從其傳送的訊號或資料。

這是IO一個完整的定義,不是特別好理解,要釐清IO這個概念,我們需要從如下兩個視角來理解它。

計算機視角理解IO

馮•諾伊曼計算機的基本思想中有提到計算機硬體組成應為五大部分:控制器,運算器,儲存器,輸入和輸出。其中輸入是指將資料輸入到計算機的裝置,輸出是指從計算機中獲取資料的裝置。對於計算機而言,任何涉及到計算機核心(CPU和記憶體)與其他裝置間的資料轉移的過程就是IO。

IO 對於計算機而言,有兩層意思:

  • IO 裝置。比如我們最常見的印表機、滑鼠、鍵盤

  • 對IO裝置的資料讀寫

程式視角理解IO

程式視角我們關注的則是應用程式本身。我們知道應用程式只有載入到記憶體中作為一個程式才能執行,它需要時刻與計算機進行資料交換,比如讀寫磁碟、遠端呼叫、訪問記憶體等等,但是作業系統為了能夠正常平穩地執行下去,它是不會執行應用程式隨意訪問計算機硬體部分,如記憶體、硬碟、網路卡,應用程式必須通過作業系統提供的API來訪問,以達到安全的訪問控制。所以應用程式如果要訪問核心管理的IO,則必須通過有作業系統提供的API來間接訪問。所以 IO對應應用程式而言,強調的則是 通過向核心發起系統呼叫完成對I/O的間接訪問

所以,換句話說應用程式發起一次IO訪問是分為兩個階段的:

  1. IO 呼叫階段:應用程式向核心發起系統呼叫。

  2. IO執行階段:核心執行IO操作並返回。

    1. 資料準備階段:核心等待IO裝置準備好資料
    2. 資料拷貝階段:將資料從核心緩衝區拷貝到使用者空間緩衝區

使用者空間&核心空間

作業系統是利用CPU 指令來計算和控制計算機系統的,有些指令很溫和,我們操作它不會對作業系統產生什麼危害,而有些指令則非常危險,如果使用不當則會導致系統崩潰,如果作業系統允許所有的應用程式能夠直接訪問這些很危險的指令,這會讓計算機大大增加崩潰的概率。所以作業系統為了更加地保護自己,則將這些危險的指令保護起來,不允許應用程式直接訪問。

現代作業系統都是採用虛擬儲存器,作業系統為了保護危險指令被應用程式直接訪問,則將虛擬空間劃分為核心空間和使用者空間。

  • 核心空間則是作業系統的核心,它提供作業系統的最基本的功能,是作業系統工作的基礎,它負責管理系統的程式、記憶體、裝置驅動程式、檔案和網路系統,決定著系統的效能和穩定性。

  • 使用者空間,非核心應用程式則執行在使用者空間。使用者空間中的程式碼執行在較低的特權級別上,只能看到允許它們使用的部分系統資源,並且不能使用某些特定的系統功能,也不能直接訪問核心空間和硬體裝置,以及其他一些具體的使用限制。

進行空間劃分後,使用者空間通過作業系統提供的API間接訪問作業系統的核心,提高了作業系統的穩定性和可用性。

想要詳細瞭解Linux 核心空間和使用者空間,則可以關注如下兩篇文章:

使用者態和核心態程式切換

  • 核心態: CPU可以訪問記憶體所有資料, 包括外圍裝置, 例如硬碟,、網路卡,CPU也可以將自己從一個程式切換到另一個程式。

  • 使用者態: 只能受限的訪問記憶體, 且不允許訪問外圍裝置。佔用CPU的能力被剝奪, CPU資源可以被其他程式獲取。

我們知道CPU為了保護作業系統,將空間劃分為核心空間和使用者空間,程式既可以在核心空間執行,也可以在使用者空間執行。當程式執行在核心空間時,它就處在核心態,當程式執行在使用者空間時,他就是使用者態。開始所有應用程式都是執行在使用者空間的,這個時候它是使用者態,但是它想做一些只有核心空間才能做的事情,如讀取IO,這個時候程式需要通過系統呼叫來訪問核心空間,程式則需要從使用者態轉變為核心態。

使用者態和核心態之間的切換開銷有點兒大,那它開銷在哪裡呢?有如下幾點:

  • 保留使用者態現場(上下文、暫存器、使用者棧等)

  • 複製使用者態引數,使用者棧切到核心棧,進入核心態

  • 額外的檢查(因為核心程式碼對使用者不信任)

  • 執行核心態程式碼

  • 複製核心態程式碼執行結果,回到使用者態

  • 恢復使用者態現場(上下文、暫存器、使用者棧等)

所以,頻繁的IO操作會頻繁的造成使用者態 —> 核心態 —> 使用者態的切換,這嚴重會影響系統效能。後面小編會介紹IO的一些優化,重點就是減少切換。

好了,就到這裡了,想要詳細瞭解使用者態和核心態,可以關注如下兩篇文章:

五種IO模型

《UNIX網路程式設計》說得很清楚,5種IO模型分別是 阻塞IO模型非阻塞IO模型IO複用模型訊號驅動IO模型非同步IO模型。前4種為同步IO操作,只有非同步IO模型是非同步IO操作。

這裡小編問個問題,為什麼前四種是同步,而只有非同步IO模型才是非同步呢?

阻塞IO模型

阻塞IO模型是最常見最簡單的IO模型,圖如下:

應用程式發起一個系統呼叫(recvform),這個時候應用程式會一直阻塞下去,直到核心把資料準備好,並將其從核心複製到使用者空間,複製完成後返回成功提示,這個時候應用程式才會繼續處理資料。

所以,阻塞IO模型在IO兩個階段都會阻塞

  • 優點

    • 模型簡單,實現難度低

    • 適用於併發量較小的應用開發

  • 缺點

    • 整個過程都阻塞,程式一直掛起,程式效能較為低,不適用併發大的應用

場景
某天,你跟你女朋友(假如你有女朋友)去飯店吃飯,點完餐後,你就做坐那裡一直等菜做好後,吃飽喝足才離開。這期間你和你女朋友由於擔心不知道菜什麼時候才能做好,所以這個期間你們就只能一直在座位上面等著,什麼時候也不能幹。

非阻塞 IO模型

非阻塞IO模型圖例如下:

應用程式發起recvform系統呼叫,如果資料包沒有準備會則會立即返回一個EWOULDBLOCK錯誤碼,程式並不需要進行等待。程式收到該錯誤後,判斷核心資料還沒有準備好,它還可以繼續傳送 recvform,如果資料包已經準備好了,待資料從核心拷貝到使用者空間返回成功指示後,程式則可以處理資料包了,

所以, 非阻塞IO模型需要應用程式不斷地主動詢問核心資料是否已準備好了

  • 優點

    • 模型簡單,實現難度低

    • 與阻塞IO模型對比,它在等待資料包的過程中,程式並沒有阻塞,它可以做其他的事情

  • 缺點

    • 輪詢傳送 recvform ,消耗CPU 資源

    • 與阻塞IO模型一樣,它也不適用於併發量大的應用程式

場景
一個星期後,你跟你女朋友還是去那家餐廳吃飯,點完菜後,你女朋友吸取上次教訓,知道要在這裡乾等,所以還不如去逛逛,買點香水口紅啥的。但是呢,由於你們擔心會錯過上菜,所以你們就每隔一段時間就來問下服務員,你們的菜準備好了沒有,來來回回好多回,若干次後,終於問到菜已經準備好了,然後你們就開心的吃起來。

IO複用模型

基於非阻塞IO模型,我們知道,它需要程式不斷地輪詢發起recvform系統呼叫,在整個過程中,輪詢會佔據很大一部分過程,而且不斷輪詢是很消耗CPU的。而且我們又不是隻有一個程式在這裡發起recvform系統低呼叫,有可能是幾萬幾十萬個。

我們可以想象這樣一個場景,你是喜茶服務員,每個人點好奶茶後,都會過來問你他的奶茶好了沒有,三五個人你頂得住,那幾十上百個人呢?而且他們又不是隻問一次,而是每隔幾分鐘就來問你一次,就問你煩不煩?我估計你都會懷疑人生了。那有辦法解決沒有呢?這麼多人來問你受不了,一個人問不就可以解決了?奶茶做好了,由他來通知你們不就可以了?

IO複用模型採用的就是這種方式,不需要所有程式輪詢來發起recvform來查詢資料是否已經準備好了,而是有人幫忙來詢問,這個幫忙的人就是select

IO複用模型圖例如下:

多個程式的IO註冊到一個複用器(select)上,然後用一個程式監聽該 select,select 會監聽所有註冊進來的IO。如果核心的資料包沒有準備好,select 呼叫程式會被阻塞,而當任一IO在核心緩衝區中有資料,select呼叫就會返回可讀條件,然後程式再進行recvform系統呼叫,核心將資料拷貝到使用者空間,注意這個過程是阻塞的。

  • 優點

    • 一個進行負責狀態監聽,效能較好。

    • 適用於高併發應用程式

  • 缺點

    • 模型複雜,實現、開發難度較大

場景
還是那家餐廳,開始的時候,大家都是在那裡等,服務員只需要等菜做好,端上來就可以了,某天有些小夥伴發現你跟你女朋友竟然利用等菜的空閒時間去逛街(雖然累,但好歹也買了幾件東西對吧),然後他們也採用了這種方式,這個時候服務員就受不了了,你們隔一段時間就來問,隔一段時間就來問,煩都煩死了,於是他想了一個辦法,說,你們派一個人來問就可以了,我這邊做了由他來告訴你們菜是否已經做了好。

後面小編會有專門的一篇文章來分析IO多路複用,敬請期待!!

訊號驅動IO模型

IO 複用模型在第一個階段和第二個階段其實都有阻塞,第一個階段阻塞於 select 呼叫,第二個階段阻塞於資料複製,那有沒有辦法在第一個階段或者第二個階段不阻塞,進一步提升效能呢?訊號驅動IO模型。圖例如下:

程式發起一個IO操作,會向核心註冊一個訊號處理程式,然後 立即返回不阻塞,當核心將資料包準備好後會傳送一個訊號給程式,這時候程式便可以在訊號處理程式中呼叫IO處理資料包。它與IO複用模型的主要區別是等待資料階段無阻塞。

  • 優點

    • 採用回撥機制,等待資料階段無阻塞

    • 適用於高併發應用程式

  • 缺點

    • 模型較為複雜,實現起來有點兒困難

場景
有人幫你問,其實也不是那麼好,因為你還是要等他來告訴你,而且他是隻要你們當中有一個人的菜做好了就告訴你們所有人。於是,你們又想了一種方案,我點完菜後,我告訴服務員,我留我的微信在你這裡,菜做好後,你告訴我就可以了。這樣你女票就可以利用這個空餘時間逛更久了。

非同步IO模型

訊號驅動IO模型,進一步優化了IO操作流程,經過了三輪優化,它終於不用在資料等待階段阻塞了,但是在資料複製節點依然是阻塞的,所以如果我們需要進一步優化的話,只需要把第二個階段也進一步優化為非同步,我們就大功告成了,也就變成了真正的非同步IO了。

當程式傳送一個IO操作,程式會立刻返回(不阻塞),但是也不能發揮結果,核心會把整個IO資料包準備好後,再通知程式,程式再處理資料包。

場景
經過了前面四次的

  • 優點

    • 整個過程都不阻塞,一步到位

    • 非常使用高併發應用

  • 缺點

    • 需要作業系統的底層支援,LINUX 2.5 版本核心首現,2.6 版本產品的核心標準特性

    • 模型複雜,實現、開發難度較大

場景
雖然留電話的方式不錯,你只需要留一個微信就可以了,瞬間解放了,後面你又發現了一個問題,你到了餐廳後,還不能立刻吃飯,因為他們還要上菜,這個過程你還是要等,如果你到店後立刻就可以吃難道不是更爽麼?所以你服務員溝通說,你做好菜後,直接上,完成後再微信通知你,你到店後就直接吃了。這樣你是不是更加爽了?

總結

五種IO模型,層層遞進,一個比一個效能高,當然模型的複雜度也一個比一個複雜。最後用一張圖來總結下

通過這張圖,我想你應該可以回答文章前面的那個問題了。

參考

相關文章