半小時搞懂 IO 模型

1. 背景

最近在折騰網絡編程,發現 IO 模型這塊比較模糊,翻了不少資料,這裏總結分享下。 關鍵字:網絡編程;IO 模型

2. 前置知識一:內核態,用戶態

想要弄懂 IO 模型,有一批前置知識需要掌握,首先是內核態和用戶態的概念。操作系統爲了保護自己,設計了用戶態、內核態兩個狀態。應用程序一般工作在用戶態,當調用一些底層操作的時候(比如 IO 操作),就需要切換到內核態纔可以進行。用戶態和內核態的切換需要消耗一些資源,零拷貝技術就是通過減少用戶態和內核態的轉換來提高性能的。

3. 前置知識二:應用程序從網絡中接收數據的大致流程

服務器從網絡接收的大致流程如下:

  1. 數據通過計算機網絡來到了網卡

  2. 把網卡的數據讀取到 socket 緩衝區

  3. 把 socket 緩衝區讀取到用戶緩衝區,之後應用程序就可以使用了

核心就是兩次讀取操作,五大 IO 模型的不同之處也就在於這兩個讀取操作怎麼交互。

4. 前置知識三:理解同步 / 異步、阻塞 / 非阻塞

同步 / 異步:這個是應用層面的概念,指的是調用一個函數,我們是等這個函數執行完再繼續執行下一步,還是調完函數就繼續執行下一步,另起一個線程去執行所調用的函數。關注的是線程間的協作。阻塞 / 非阻塞,這個是硬件層面的概念,阻塞是指 cpu “被”休息,處理其他進程去了,比如 IO 操作,而非阻塞則是 cpu 仍然會執行,不會切換到其他進程。關注的是 CPU 會不會 “被” 休息,表現在應用層面就是線程會不會 “被” 掛起。至於同步和阻塞有什麼區別,異步和非阻塞有什麼區別,其實這是不同層面的東西,不好相互比較的。在學習 IO 模型的過程中,千萬別鑽這個牛角尖。

5. 前置知識四:理解同步阻塞、同步非阻塞、異步阻塞、異步非阻塞

有很多 IO 模型的博客,會把同步 / 異步、阻塞 / 非阻塞兩兩組合,把 IO 模型分成四類。

初看其實很納悶的,都異步了,還咋阻塞啊?

其實大可不必糾結這個,同步 / 異步、阻塞 / 非阻塞本身就是不同層面的東西,強行組合起來就是不好理解,甚至是錯誤的。

建議是拋開這個,直接去理解五大 IO 模型,千萬別鑽牛角尖。其實,真要分,也只能拆成兩個維度分,而不是四個維度。首先是按阻塞 / 非阻塞分:

然後是按同步 / 異步分:

6. 五大 IO 模型之:阻塞 IO

好了,如果掌握了前面提到的的這些前置知識,理解 IO 模型就稍微輕鬆點了,現在開始。

之前提了,應用程序從網絡中接收數據的大致流程就是兩步:

  1. 數據準備:等待網絡數據,把網卡的數據讀取到 socket 緩衝區

  2. 數據複製:把 socket 緩衝區的數據讀取到用戶態 Buffer,供應用程序使用

IO 模型的不同之處也就在於這兩個操作怎麼交互,我們先看看阻塞 IO 模型

當應用程序發起 read 調用時,調用線程會阻塞住直到第一步讀取操作的完成。等第一步讀取操作完成後,會將數據讀取到用戶態 Buffer 中,這個過程中調用線程仍然是阻塞的,直到數據複製完成,整個流程用圖來表示就張這樣:

這種 IO 模型的好處就是好理解,API 簡單好上手,適用於連接數不多的網絡應用。

7. 五大 IO 模型之:非阻塞 IO

當應用程序發起 read 調用時,如果沒有數據可讀,調用線程不會阻塞。但應用程序爲了讀到數據,就會一直循環調用,直到有數據可讀。

等第一步讀取操作完成後,第二步就和阻塞 IO 一樣了。會將數據讀取到用戶態 Buffer 中,這個過程中調用線程仍然是阻塞的,直到數據複製完成,整個流程用圖來表示就張這樣:

這種 IO 模型的並沒有特別好處,而且會一直循環調用底層的接口,性能堪憂,很少使用。

8. 五大 IO 模型之:信號驅動 IO

當應用程序發起 read 調用,註冊一個 handler,等待有數據後的回調。應用程序一旦被回調,就說明數據已經可以讀取了,就會進行第二步操作,把數據讀取到用戶態 Buffer 中。同樣,第二步仍然是阻塞的。

這種 IO 模型的好處就是相比於非阻塞 IO,使用通知 & 回調機制減少了循環的開銷,但是對於連接數多的場景,可能會因爲信號隊列溢出導致沒法通知,用的不多。

9. 五大 IO 模型之:多路複用 IO

當應用程序發起 read 調用時,如果沒有數據可讀,調用線程不會阻塞,系統會把 socket 註冊到一個 “多路複用器” 上,等到有數據了會把可讀的 socket 加入隊列,供應用層使用。

大概的代碼如下:

java複製代碼while (true) {
    if (selector.select(READ_KEY) > 0) { // selector 就是多路複用器,READ_KEY 大於 1 說明有可讀的socket
        Set<SelectionKey> set = clientSelector.selectedKeys();
        Iterator<SelectionKey> keyIterator = set.iterator();
        while (keyIterator.hasNext()) { 
            SelectionKey key = keyIterator.next();
            if (key.isReadable()) {
                // 讀取數據
            }
        }
    }
}

這種 IO 模型的好處是能夠應對大量的連接,尤其適用於大量的短連接。現在大多數網絡應用,底層採用的都是多路複用 IO。

10. 五大 IO 模型之:異步 IO

異步 IO 則和上面四種 IO 模型都不通,他是完完全全的異步,兩步操作都不會阻塞。應用程序發起 read 調用後,等收到回調通知,就可以去使用用戶態 Buffer 的數據了,如下圖所示。

11. 打個比方

打個個人認爲很貼切的比方,幫助理解。

大家都去醫院取過藥吧,五種 IO 模型就像是不同的取藥方式。

12. 可能會產生的疑問:

12.1 Java 的 nio 是對多路複用 IO 模型的實現,爲什麼叫非阻塞?

首先 Java 的 nio 包可以用來實現多路複用 IO 模型,也可以用來實現非阻塞 IO 模型,只不過非阻塞 IO 模型性能差沒人用而已。其次,nio 中的那個 “n” 是 new 的意思。當時 JDK 的開發者爲了和老的 io 包做區分,才用 nio 來表示的,並不是 nonblocking 的 “n”,所以叫“新 IO 包” 更準確,也不容易弄混。

12.2 select、poll、epoll 有什麼關係

select、poll、epoll 都是用來實現多路複用的,原理也都是通過遍歷找到可讀寫的 socket,區別在於

12.3 信號驅動 IO 和多路複用 IO 很難分辨

信號驅動 IO 的底層機制是事件通知,多路複用 IO 的底層機制是遍歷 + 回調,只不過在應用層面包裝成了事件而已。

13. 總結

從網卡中讀取數據有兩步:第一步是網卡到 socket 緩存區,第二步是從 socket 緩衝區到內核態。

IO 模型有五種:阻塞 IO、非阻塞 IO、信號驅動 IO、多路複用 IO、異步 IO。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1YwsbpyZcm60rDyyFiyoJA