最近越來越認為,在講解技術(shù)相關(guān)問題時,大白話固然很重要,通俗易懂,讓人有想讀下去的欲望。但幾乎所有的事,都有兩面性,在看到其帶來好處時,不妨想想是否也引入了不好的地方。
例如在博客中,過于大白話的語言的確會讓你閱讀起來更加順暢,也更容易理解。但這都是其他人理解,已經(jīng)咀嚼過了的,人家是已經(jīng)完全理解了,你從這些信息中大概可能會觀察不到全貌。所以,適當(dāng)?shù)陌自捠呛芎玫模@個度得控制一下。
接下來切入正文。
相信大家經(jīng)常看到這個問題:
BIO、NIO 和 AIO 有什么區(qū)別?
看到這個問題,可能你腦海中就會浮現(xiàn)以下這些字眼。比如 BIO 就是如果從內(nèi)核獲取數(shù)據(jù)會一直阻塞,直到數(shù)據(jù)準(zhǔn)備完畢返回。再比如 NIO,內(nèi)核在數(shù)據(jù)沒有準(zhǔn)備好時不會阻塞住,調(diào)用程序會一直詢問內(nèi)核數(shù)據(jù)是否 Ready。
雖然是正確的,字?jǐn)?shù)也很少。但是這樣一來,你看這些概念就不是理解,而是背誦了。其實 BIO 和 NIO 這類的名詞還有一個共同的名字叫——IO模型,總共有:
IO 模型
由于信號驅(qū)動 IO 在實際中不常用,我們主要講以下四種模型:
- 同步阻塞
- 同步非阻塞
- IO 多路復(fù)用
- 異步 IO
這里還是通過例子來理解這 4 種 IO 模型:
假設(shè)此時客戶端正在發(fā)送一些數(shù)據(jù)到服務(wù)器,并且數(shù)據(jù)已經(jīng)通過客戶端的協(xié)議棧、網(wǎng)卡,陸陸續(xù)續(xù)的到達了服務(wù)器這邊的內(nèi)核態(tài) Buffer 中了。
不清楚用戶態(tài)和內(nèi)核態(tài)區(qū)別的可以看看《簡單聊聊用戶態(tài)和內(nèi)核態(tài)的區(qū)別》
對數(shù)據(jù)在網(wǎng)絡(luò)中是如何傳輸?shù)募毠?jié)感興趣的,可以去看看我之前寫的文章 《請求數(shù)據(jù)包從發(fā)送到接收,都經(jīng)歷了什么?》。
同步阻塞 BIO
我們需要知道,內(nèi)核在處理數(shù)據(jù)的時候其實是分成了兩個階段:
- 數(shù)據(jù)準(zhǔn)備
- 數(shù)據(jù)復(fù)制
在網(wǎng)絡(luò) IO 中,數(shù)據(jù)準(zhǔn)備可能是客戶端還有部分?jǐn)?shù)據(jù)還沒有發(fā)送、或者正在發(fā)送的途中,當(dāng)前內(nèi)核 Buffer 中的數(shù)據(jù)并不完整;而數(shù)據(jù)復(fù)制則是將內(nèi)核態(tài) Buffer 中的數(shù)據(jù)復(fù)制到用戶態(tài)的 Buffer 中去。
當(dāng)調(diào)用線程發(fā)起 read 系統(tǒng)調(diào)用時,如果此時內(nèi)核數(shù)據(jù)還沒有 Ready,調(diào)用線程會阻塞住,等待內(nèi)核 Buffer 的數(shù)據(jù)。內(nèi)核數(shù)據(jù)準(zhǔn)備就緒之后,會將內(nèi)核態(tài) Buffer 的數(shù)據(jù)復(fù)制到用戶態(tài) Buffer 中,這個過程中調(diào)用線程仍然是阻塞的,直到數(shù)據(jù)復(fù)制完成,整個流程用圖來表示就張這樣:
同步非阻塞 NIO
相信大家知道 Java 中有個包叫 nio,但那跟我們現(xiàn)在正在討論的 NIO 不是同一個概念。
現(xiàn)在正在討論的是 Non-Blocking IO,代表同步非阻塞,是一種基礎(chǔ)的 IO 模型。而 nio 包則是 New IO,里面的 IO 模型實際上是 IO多路復(fù)用,大家不要搞混淆了。
有了 BIO 的基礎(chǔ),這次我們直接來看圖:
NIO
還是分為兩個階段來討論。
數(shù)據(jù)準(zhǔn)備階段。此時用戶線程發(fā)起 read 系統(tǒng)調(diào)用,此時內(nèi)核會立即返回一個錯誤,告訴用戶態(tài)數(shù)據(jù)還沒有 Read,然后用戶線程不停地發(fā)起請求,詢問內(nèi)核當(dāng)前數(shù)據(jù)的狀態(tài)。
數(shù)據(jù)復(fù)制階段。此時用戶線程還在不斷的發(fā)起請求,但是當(dāng)數(shù)據(jù) Ready 之后,用戶線程就會陷入阻塞,直到數(shù)據(jù)從內(nèi)核態(tài)復(fù)制到用戶態(tài)。
稍微總結(jié)一下,如果內(nèi)核態(tài)的數(shù)據(jù)沒有 Ready,用戶線程不會阻塞;但是如果內(nèi)核態(tài)數(shù)據(jù) Ready 了,即使當(dāng)前的 IO 模型是同步非阻塞,用戶線程仍然會進入阻塞狀態(tài),直到數(shù)據(jù)復(fù)制完成,并不是絕對的非阻塞。
那 NIO 的好處是啥呢?顯而易見,實時性好,內(nèi)核態(tài)數(shù)據(jù)沒有 Ready 會立即返回。但是事情的兩面性就來了,頻繁的輪詢內(nèi)核,會占用大量的 CPU 資源,降低效率。
IO 多路復(fù)用
IO 多路復(fù)用實際上就解決了 NIO 中的頻繁輪詢 CPU 的問題。在之前的 BIO 和 NIO 中只涉及到一種系統(tǒng)調(diào)用——read,在 IO 多路復(fù)用中要引入新的系統(tǒng)調(diào)用——select。
read 用于讀取內(nèi)核態(tài) Buffer 中的數(shù)據(jù),而 select 你可以理解成 MySQL 中的同名關(guān)鍵字,用于查詢 IO 的就緒狀態(tài)。
在 NIO 中,內(nèi)核態(tài)數(shù)據(jù)沒有 Ready 會導(dǎo)致用戶線程不停的輪詢,從而拉滿 CPU。而在 IO 多路復(fù)用中調(diào)用了 select 之后,只要數(shù)據(jù)沒有準(zhǔn)備好,用戶線程就會阻塞住,避免了頻繁的輪詢當(dāng)前的 IO 狀態(tài),用圖來表示的話是這樣:
IO 多路復(fù)用
異步 AIO
該模型的實現(xiàn)就如其名,是異步的。用戶線程發(fā)起 read 系統(tǒng)調(diào)用之后,無論內(nèi)核 Buffer 數(shù)據(jù)是否 Ready,都不會阻塞,而是立即返回。
內(nèi)核在收到請求之后,會開始準(zhǔn)備數(shù)據(jù),準(zhǔn)備好了&復(fù)制完成之后會由內(nèi)核發(fā)送一個 Signal 給用戶線程,或者回調(diào)用戶線程注冊的接口進行通知。用戶線程收到通知之后就可以去讀取用戶態(tài) Buffer 的數(shù)據(jù)了。
AIO
由于這種實現(xiàn)方式,異步 IO 有時也被叫做信號驅(qū)動 IO。相信你也發(fā)現(xiàn)了,這種方式最重要的是需要 OS 的支持,如果 OS 不支持就直接完蛋。
Linux 系統(tǒng)在 2.6 版本的時候才引入了異步IO,不過那個時候并不算真正的異步 IO,因為內(nèi)核并不支持,底層其實是通過 IO 多路復(fù)用實現(xiàn)的。而到了 Linux 5.1 時,才通過 io_uring 實現(xiàn)了真 AIO。
原文鏈接:https://mp.weixin.qq.com/s/8v66WyAaYJ4GOU_fdFOJyA