圖解四種 IO 模型

detectiveHLH發表於2022-01-05

最近越來越認為,在講解技術相關問題時,大白話固然很重要,通俗易懂,讓人有想讀下去的慾望。但幾乎所有的事,都有兩面性,在看到其帶來好處時,不妨想想是否也引入了不好的地方。

例如在部落格中,過於大白話的語言的確會讓你閱讀起來更加順暢,也更容易理解。但這都是其他人理解,已經咀嚼過了的,人家是已經完全理解了,你從這些資訊中大概可能會觀察不到全貌。所以,適當的白話是很好的,但這個度得控制一下。

接下來切入正文。

相信大家經常看到這個問題:

BIO、NIO 和 AIO 有什麼區別?

看到這個問題,可能你腦海中就會浮現以下這些字眼。比如 BIO 就是如果從核心獲取資料會一直阻塞,直到資料準備完畢返回。再比如 NIO,核心在資料沒有準備好時不會阻塞住,呼叫程式會一直詢問核心資料是否 Ready。

雖然是正確的,字數也很少。但是這樣一來,你看這些概念就不是理解,而是背誦了。其實 BIO 和 NIO 這類的名詞還有一個共同的名字叫——IO模型,總共有:

IO 模型
IO 模型

由於訊號驅動 IO 在實際中不常用,我們主要講以下四種模型:

  1. 同步阻塞
  2. 同步非阻塞
  3. IO 多路複用
  4. 非同步 IO

這裡還是通過例子來理解這 4 種 IO 模型:

假設此時客戶端正在傳送一些資料到伺服器,並且資料已經通過客戶端的協議棧、網路卡,陸陸續續的到達了伺服器這邊的核心態 Buffer 中了。

不清楚使用者態和核心態區別的可以看看《簡單聊聊使用者態和核心態的區別》

對資料在網路中是如何傳輸的細節感興趣的,可以去看看我之前寫的文章 《請求資料包從傳送到接收,都經歷了什麼?》

同步阻塞 BIO

我們需要知道,核心在處理資料的時候其實是分成了兩個階段:

  • 資料準備
  • 資料複製

在網路 IO 中,資料準備可能是客戶端還有部分資料還沒有傳送、或者正在傳送的途中,當前核心 Buffer 中的資料並不完整;而資料複製則是將核心態 Buffer 中的資料複製到使用者態的 Buffer 中去。

呼叫執行緒發起 read 系統呼叫時,如果此時核心資料還沒有 Ready,呼叫執行緒會阻塞住,等待核心 Buffer 的資料。核心資料準備就緒之後,會將核心態 Buffer 的資料複製到使用者態 Buffer 中,這個過程中呼叫執行緒仍然是阻塞的,直到資料複製完成,整個流程用圖來表示就張這樣:

同步非阻塞 NIO

相信大家知道 Java 中有個包叫 nio,但那跟我們現在正在討論的 NIO 不是同一個概念。

現在正在討論的是 Non-Blocking IO,代表同步非阻塞,是一種基礎的 IO 模型。而 nio 包則是 New IO,裡面的 IO 模型實際上是 IO多路複用,大家不要搞混淆了。

有了 BIO 的基礎,這次我們直接來看圖:

NIO
NIO

還是分為兩個階段來討論。

資料準備階段。此時使用者執行緒發起 read 系統呼叫,此時核心會立即返回一個錯誤,告訴使用者態資料還沒有 Read,然後使用者執行緒不停地發起請求,詢問核心當前資料的狀態。

資料複製階段。此時使用者執行緒還在不斷的發起請求,但是當資料 Ready 之後,使用者執行緒就會陷入阻塞直到資料從核心態複製到使用者態

稍微總結一下,如果核心態的資料沒有 Ready,使用者執行緒不會阻塞;但是如果核心態資料 Ready 了,即使當前的 IO 模型是同步非阻塞,使用者執行緒仍然會進入阻塞狀態,直到資料複製完成,並不是絕對的非阻塞。

那 NIO 的好處是啥呢?顯而易見,實時性好,核心態資料沒有 Ready 會立即返回。但是事情的兩面性就來了,頻繁的輪詢核心,會佔用大量的 CPU 資源,降低效率

IO 多路複用

IO 多路複用實際上就解決了 NIO 中的頻繁輪詢 CPU 的問題。在之前的 BIO 和 NIO 中只涉及到一種系統呼叫——read,在 IO 多路複用中要引入新的系統呼叫——select

read 用於讀取核心態 Buffer 中的資料,而 select 你可以理解成 MySQL 中的同名關鍵字,用於查詢 IO 的就緒狀態。

在 NIO 中,核心態資料沒有 Ready 會導致使用者執行緒不停的輪詢,從而拉滿 CPU。而在 IO 多路複用中呼叫了 select 之後,只要資料沒有準備好,使用者執行緒就會阻塞住,避免了頻繁的輪詢當前的 IO 狀態,用圖來表示的話是這樣:

IO 多路複用
IO 多路複用

非同步 AIO

該模型的實現就如其名,是非同步的。使用者執行緒發起 read 系統呼叫之後,無論核心 Buffer 資料是否 Ready,都不會阻塞,而是立即返回。

核心在收到請求之後,會開始準備資料,準備好了&複製完成之後會由核心傳送一個 Signal 給使用者執行緒,或者回撥使用者執行緒註冊的介面進行通知。使用者執行緒收到通知之後就可以去讀取使用者態 Buffer 的資料了。

AIO
AIO

由於這種實現方式,非同步 IO 有時也被叫做訊號驅動 IO。相信你也發現了,這種方式最重要的是需要 OS 的支援,如果 OS 不支援就直接完蛋。

Linux 系統在 2.6 版本的時候才引入了非同步IO,不過那個時候並不算真正的非同步 IO,因為核心並不支援,底層其實是通過 IO 多路複用實現的。而到了 Linux 5.1 時,才通過 io_uring 實現了真 AIO。

歡迎微信搜尋關注【SH的全棧筆記】,如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章