看一遍就理解:IO 模型詳解
前言
大家好,我是程序員田螺。今天我們一起來學習 IO 模型。在本文開始前呢,先問問大家幾個問題哈~
什麼是 IO 呢?什麼是阻塞非阻塞 IO?什麼是同步異步 IO?什麼是 IO 多路複用?select/epoll 跟 IO 模型有什麼關係?有幾種經典 IO 模型呢?BIO、NIO、AIO 到底有什麼區別的?
如果這些問題,你都能很好答上的話,那恭喜你,你對 IO 的掌握已經很棒啦!那你跟田螺哥一起看完這篇文章,再複習一下,加深印象吧~ 如果你對這些問題模棱兩可的話,那也沒關係,看完這篇文章,就理解啦!
什麼是 IO 呢?
IO,英文全稱是 Input/Output,翻譯過來就是輸入 / 輸出。平時我們聽得挺多,就是什麼磁盤 IO,網絡 IO。那 IO 到底是什麼呢?是不是有種懵懵懂懂的感覺呀,好像大概知道它是什麼,又好像說不清楚。
IO,即輸入 / 輸出,到底誰是輸入?誰是輸出呢?IO 如果脫離了主體,就會讓人疑惑。
計算機角度的 IO
我們常說的輸入輸出,比較直觀的意思就是計算機的輸入輸出,計算機就是主體。大家是否還記得,大學學計算機組成原理的時候,有個馮. 諾依曼結構,它將計算機分成分爲 5 個部分:運算器、控制器、存儲器、輸入設備、輸出設備。
輸入設備是向計算機輸入數據和信息的設備,鍵盤,鼠標都屬於輸入設備;輸出設備是計算機硬件系統的終端設備,用於接收計算機數據的輸出顯示,一般顯示器、打印機屬於輸出設備。
例如你在鼠標鍵盤敲幾下,它就會把你的指令數據,傳給主機,主機通過運算後,把返回的數據信息,輸出到顯示器。
鼠標、顯示器這只是直觀表面的輸入輸出,回到計算機架構來說,涉及計算機核心與其他設備間數據遷移的過程,就是 IO。如磁盤 IO,就是從磁盤讀取數據到內存,這算一次輸入,對應的,將內存中的數據寫入磁盤,就算輸出。這就是 IO 的本質。
操作系統的 IO
我們要將內存中的數據寫入到磁盤的話,主體會是什麼呢?主體可能是一個應用程序,比如一個 Java 進程(假設網絡傳來二進制流,一個 Java 進程可以把它寫入到磁盤)。
操作系統負責計算機的資源管理和進程的調度。我們電腦上跑着的應用程序,其實是需要經過操作系統,才能做一些特殊操作,如磁盤文件讀寫、內存的讀寫等等。因爲這些都是比較危險的操作,不可以由應用程序亂來,只能交給底層操作系統來。也就是說,你的應用程序要把數據寫入磁盤,只能通過調用操作系統開放出來的 API 來操作。
什麼是用戶空間?什麼是內核空間?
以 32 位操作系統爲例,它爲每一個進程都分配了 4G(2 的 32 次方) 的內存空間。這 4G 可訪問的內存空間分爲二部分,一部分是用戶空間,一部分是內核空間。內核空間是操作系統內核訪問的區域,是受保護的內存空間,而用戶空間是用戶應用程序訪問的內存區域。
我們應用程序是跑在用戶空間的,它不存在實質的 IO 過程,真正的 IO 是在操作系統執行的。即應用程序的 IO 操作分爲兩種動作:IO 調用和 IO 執行。IO 調用是由進程(應用程序的運行態)發起,而 IO 執行是操作系統內核的工作。此時所說的 IO 是應用程序對操作系統 IO 功能的一次觸發,即 IO 調用。
操作系統的一次 IO 過程
應用程序發起的一次 IO 操作包含兩個階段:
-
IO 調用:應用程序進程向操作系統內核發起調用。
-
IO 執行:操作系統內核完成 IO 操作。
操作系統內核完成 IO 操作還包括兩個過程:
-
準備數據階段:內核等待 I/O 設備準備好數據
-
拷貝數據階段:將數據從內核緩衝區拷貝到用戶進程緩衝區
其實 IO 就是把進程的內部數據轉移到外部設備,或者把外部設備的數據遷移到進程內部。外部設備一般指硬盤、socket 通訊的網卡。一個完整的 IO 過程包括以下幾個步驟:
-
應用程序進程向操作系統發起 IO 調用請求
-
操作系統準備數據,把 IO 外部設備的數據,加載到內核緩衝區
-
操作系統拷貝數據,即將內核緩衝區的數據,拷貝到用戶進程緩衝區
阻塞 IO 模型
我們已經知道 IO 是什麼啦,那什麼是阻塞 IO 呢?
假設應用程序的進程發起 IO 調用,但是如果內核的數據還沒準備好的話,那應用程序進程就一直在阻塞等待,一直等到內核數據準備好了,從內核拷貝到用戶空間,才返回成功提示,此次 IO 操作,稱之爲阻塞 IO。
-
阻塞 IO 比較經典的應用就是阻塞 socket、Java BIO。
-
阻塞 IO 的缺點就是:如果內核數據一直沒準備好,那用戶進程將一直阻塞,浪費性能,可以使用非阻塞 IO 優化。
非阻塞 IO 模型
如果內核數據還沒準備好,可以先返回錯誤信息給用戶進程,讓它不需要等待,而是通過輪詢的方式再來請求。這就是非阻塞 IO,流程圖如下:
非阻塞 IO 的流程如下:
-
應用進程向操作系統內核,發起
recvfrom
讀取數據。 -
操作系統內核數據沒有準備好,立即返回
EWOULDBLOCK
錯誤碼。 -
應用程序進程輪詢調用,繼續向操作系統內核發起
recvfrom
讀取數據。 -
操作系統內核數據準備好了,從內核緩衝區拷貝到用戶空間。
-
完成調用,返回成功提示。
非阻塞 IO 模型,簡稱 NIO,Non-Blocking IO
。它相對於阻塞 IO,雖然大幅提升了性能,但是它依然存在性能問題,即頻繁的輪詢,導致頻繁的系統調用,同樣會消耗大量的 CPU 資源。可以考慮 IO 複用模型,去解決這個問題。
IO 多路複用模型
既然 NIO 無效的輪詢會導致 CPU 資源消耗,我們等到內核數據準備好了,主動通知應用進程再去進行系統調用,那不就好了嘛?
在這之前,我們先來複習下,什麼是文件描述符 fd(File Descriptor), 它是計算機科學中的一個術語,形式上是一個非負整數。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。
IO 複用模型核心思路:系統給我們提供一類函數(如我們耳濡目染的 select、poll、epoll 函數),它們可以同時監控多個fd
的操作,任何一個返回內核數據就緒,應用進程再發起recvfrom
系統調用。
IO 多路複用之 select
應用進程通過調用 select 函數,可以同時監控多個fd
,在select
函數監控的fd
中,只要有任何一個數據狀態準備就緒了,select
函數就會返回可讀狀態,這時應用進程再發起recvfrom
請求去讀取數據。
非阻塞 IO 模型(NIO)中,需要N
(N>=1)次輪詢系統調用,然而藉助select
的 IO 多路複用模型,只需要發起一次詢問就夠了, 大大優化了性能。
但是呢,select
有幾個缺點:
-
監聽的 IO 最大連接數有限,在 Linux 系統上一般爲 1024。
-
select 函數返回後,是通過遍歷
fdset
,找到就緒的描述符fd
。(僅知道有 I/O 事件發生,卻不知是哪幾個流,所以遍歷所有流)
因爲存在連接數限制,所以後來又提出了 poll。與 select 相比,poll 解決了連接數限制問題。但是呢,select 和 poll 一樣,還是需要通過遍歷文件描述符來獲取已經就緒的socket
。如果同時連接的大量客戶端,在一時刻可能只有極少處於就緒狀態,伴隨着監視的描述符數量的增長,效率也會線性下降。
因此經典的多路複用模型epoll
誕生。
IO 多路複用之 epoll
爲了解決select/poll
存在的問題,多路複用模型epoll
誕生,它採用事件驅動來實現,流程圖如下:
epoll 先通過epoll_ctl()
來註冊一個fd
(文件描述符),一旦基於某個fd
就緒時,內核會採用回調機制,迅速激活這個fd
,當進程調用epoll_wait()
時便得到通知。這裏去掉了遍歷文件描述符的坑爹操作,而是採用監聽事件回調的機制。這就是 epoll 的亮點。
我們一起來總結一下 select、poll、epoll 的區別
epoll 明顯優化了 IO 的執行效率,但在進程調用epoll_wait()
時,仍然可能被阻塞。能不能醬紫:不用我老是去問你數據是否準備就緒,等我發出請求後,你數據準備好了通知我就行了,這就誕生了信號驅動 IO 模型。
IO 模型之信號驅動模型
信號驅動 IO 不再用主動詢問的方式去確認數據是否就緒,而是向內核發送一個信號(調用sigaction
的時候建立一個SIGIO
的信號),然後應用用戶進程可以去做別的事,不用阻塞。當內核數據準備好後,再通過SIGIO
信號通知應用進程,數據準備好後的可讀狀態。應用用戶進程收到信號之後,立即調用recvfrom
,去讀取數據。
信號驅動 IO 模型,在應用進程發出信號後,是立即返回的,不會阻塞進程。它已經有異步操作的感覺了。但是你細看上面的流程圖,發現數據複製到應用緩衝的時候,應用進程還是阻塞的。回過頭來看下,不管是 BIO,還是 NIO,還是信號驅動,在數據從內核複製到應用緩衝的時候,都是阻塞的。還有沒有優化方案呢?AIO(真正的異步 IO)!
IO 模型之異步 IO(AIO)
前面講的BIO,NIO和信號驅動
,在數據從內核複製到應用緩衝的時候,都是阻塞的,因此都不算是真正的異步。AIO
實現了 IO 全流程的非阻塞,就是應用進程發出系統調用後,是立即返回的,但是立即返回的不是處理結果,而是表示提交成功類似的意思。等內核數據準備好,將數據拷貝到用戶進程緩衝區,發送信號通知用戶進程 IO 操作執行完畢。
流程如下:
異步 IO 的優化思路很簡單,只需要向內核發送一次請求,就可以完成數據狀態詢問和數據拷貝的所有操作,並且不用阻塞等待結果。日常開發中,有類似思想的業務場景:
比如發起一筆批量轉賬,但是批量轉賬處理比較耗時,這時候後端可以先告知前端轉賬提交成功,等到結果處理完,再通知前端結果即可。
阻塞、非阻塞、同步、異步 IO 劃分
一個通俗例子讀懂 BIO、NIO、AIO
-
同步阻塞 (blocking-IO) 簡稱 BIO
-
同步非阻塞 (non-blocking-IO) 簡稱 NIO
-
異步非阻塞 (asynchronous-non-blocking-IO) 簡稱 AIO
一個經典生活的例子:
-
小明去喫同仁四季的椰子雞,就這樣在那裏排隊,等了一小時,然後纔開始喫火鍋。(BIO)
-
小紅也去同仁四季的椰子雞,她一看要等挺久的,於是去逛會商場,每次逛一下,就跑回來看看,是不是輪到她了。於是最後她既購了物,又喫上椰子雞了。(NIO)
-
小華一樣,去喫椰子雞,由於他是高級會員,所以店長說,你去商場隨便逛會吧,等下有位置,我立馬打電話給你。於是小華不用幹巴巴坐着等,也不用每過一會兒就跑回來看有沒有等到,最後也喫上了美味的椰子雞(AIO)
參考與感謝
-
程序員應該這樣理解 IO[1]
-
Linux IO 模式及 select、poll、epoll 詳解 [2]
-
IO 模型知多少 | 理論篇 [3]
-
100% 弄明白 5 種 IO 模型 [4]
參考資料
[1] 程序員應該這樣理解 IO: https://www.jianshu.com/p/fa7bdc4f3de7
[2] Linux IO 模式及 select、poll、epoll 詳解: https://segmentfault.com/a/1190000003063859
[3] IO 模型知多少 | 理論篇: https://cloud.tencent.com/developer/article/1648650
[4] 100% 弄明白 5 種 IO 模型: https://zhuanlan.zhihu.com/p/115912936
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bb7C6VNbq7REP9u8PsreSg