半小時搞懂 IO 模型
1. 背景
最近在折騰網絡編程,發現 IO 模型這塊比較模糊,翻了不少資料,這裏總結分享下。 關鍵字:網絡編程;IO 模型
2. 前置知識一:內核態,用戶態
想要弄懂 IO 模型,有一批前置知識需要掌握,首先是內核態和用戶態的概念。操作系統爲了保護自己,設計了用戶態、內核態兩個狀態。應用程序一般工作在用戶態,當調用一些底層操作的時候(比如 IO 操作),就需要切換到內核態纔可以進行。用戶態和內核態的切換需要消耗一些資源,零拷貝技術就是通過減少用戶態和內核態的轉換來提高性能的。
3. 前置知識二:應用程序從網絡中接收數據的大致流程
服務器從網絡接收的大致流程如下:
-
數據通過計算機網絡來到了網卡
-
把網卡的數據讀取到 socket 緩衝區
-
把 socket 緩衝區讀取到用戶緩衝區,之後應用程序就可以使用了
核心就是兩次讀取操作,五大 IO 模型的不同之處也就在於這兩個讀取操作怎麼交互。
4. 前置知識三:理解同步 / 異步、阻塞 / 非阻塞
同步 / 異步:這個是應用層面的概念,指的是調用一個函數,我們是等這個函數執行完再繼續執行下一步,還是調完函數就繼續執行下一步,另起一個線程去執行所調用的函數。關注的是線程間的協作。阻塞 / 非阻塞,這個是硬件層面的概念,阻塞是指 cpu “被”休息,處理其他進程去了,比如 IO 操作,而非阻塞則是 cpu 仍然會執行,不會切換到其他進程。關注的是 CPU 會不會 “被” 休息,表現在應用層面就是線程會不會 “被” 掛起。至於同步和阻塞有什麼區別,異步和非阻塞有什麼區別,其實這是不同層面的東西,不好相互比較的。在學習 IO 模型的過程中,千萬別鑽這個牛角尖。
5. 前置知識四:理解同步阻塞、同步非阻塞、異步阻塞、異步非阻塞
有很多 IO 模型的博客,會把同步 / 異步、阻塞 / 非阻塞兩兩組合,把 IO 模型分成四類。
初看其實很納悶的,都異步了,還咋阻塞啊?
其實大可不必糾結這個,同步 / 異步、阻塞 / 非阻塞本身就是不同層面的東西,強行組合起來就是不好理解,甚至是錯誤的。
建議是拋開這個,直接去理解五大 IO 模型,千萬別鑽牛角尖。其實,真要分,也只能拆成兩個維度分,而不是四個維度。首先是按阻塞 / 非阻塞分:
然後是按同步 / 異步分:
6. 五大 IO 模型之:阻塞 IO
好了,如果掌握了前面提到的的這些前置知識,理解 IO 模型就稍微輕鬆點了,現在開始。
之前提了,應用程序從網絡中接收數據的大致流程就是兩步:
-
數據準備:等待網絡數據,把網卡的數據讀取到 socket 緩衝區
-
數據複製:把 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 模型就像是不同的取藥方式。
-
阻塞 IO: 排隊等藥,排到我了但是藥還沒準備好,那我也繼續等着,別人也不能取。這種我沒好,別人也落不着好的方式,就是阻塞 IO 的體現。
-
非阻塞 IO: 排隊等藥,排到我了但是藥還沒準備好,那我重新排吧。重新排隊就是輪詢,因爲重新排了也沒有阻塞別人取消,就是非阻塞 IO 的體現。
-
信號驅動 IO: 不用排隊等藥,藥準備好了就直接短信通知你去取。短信通知就相當於信號驅動了,因爲不用排隊,節省了不少時間。
-
多路複用 IO: 這個就是日常中常見的那種取藥方式了,付了錢後要去藥房的機器上掃碼,然後盯着顯示器,上面顯示了你的名字,再去取藥。在機器上掃碼就相當於註冊,顯示了你的名字就相當於有需要處理的 IO 事件了。現實中顯示了我的名字,我還是要去排隊,這也是對應上的,因爲一個 selector 返回的是多個需要處理的 IO 時間,一個個處理就相當於一個個排隊取藥。
-
異步 IO: 這個就很賽博朋克了,異步 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,區別在於
-
select 有限制,最多 1024 個,poll、epoll 沒有這個限制。
-
poll 對數據結構有優化,沒有 1024 個的限制,但還是要遍歷所有 socket,目前很少用。
-
epoll 對遍歷有優化,不會遍歷所有 socket,只會遍歷那些可讀的 socket,所以效率有所提升。
12.3 信號驅動 IO 和多路複用 IO 很難分辨
信號驅動 IO 的底層機制是事件通知,多路複用 IO 的底層機制是遍歷 + 回調,只不過在應用層面包裝成了事件而已。
13. 總結
從網卡中讀取數據有兩步:第一步是網卡到 socket 緩存區,第二步是從 socket 緩衝區到內核態。
IO 模型有五種:阻塞 IO、非阻塞 IO、信號驅動 IO、多路複用 IO、異步 IO。
-
阻塞 IO:兩步都阻塞
-
非阻塞 IO:第一步不阻塞,但應用層不知道什麼時候數據可讀,所以需要不斷輪詢
-
信號驅動 IO:第一步不阻塞,但應用層不感知這一步的阻塞,機制是事件通知機制,數據準備好後直接通知應用層讀取
-
多路不用 IO:第一步不阻塞,但應用層不感知這一步的阻塞,機制是遍歷所有 Socket,有準備好的再通知應用層讀取
-
異步 IO:純異步,數據準備好後,應用層直接使用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1YwsbpyZcm60rDyyFiyoJA