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