文章目錄
- 1. 引言
- 2. I/O 的定義
-
- 2.1 計算機視角
- 2.2 程式視角
- 3. IO 模型之阻塞 I/O (BIO)
- 4. IO 模型之非阻塞 I/O (NIO)
- 5. IO 模型之 IO 多路複用
-
- 5.1 IO多路複用之select/poll
- 5.2 IO 多路複用之 epoll
- 6. IO 模型之信号驅動 IO (SIGIO)
- 7. IO 模型之異步 IO (AIO)
- 8. 總結
- 參考資料
1. 引言
同步異步I/O,阻塞非阻塞I/O是程式員老生常談的話題了,也是自己一直以來懵懵懂懂的一個話題。比如:何為同步異步?何為阻塞與非阻塞?二者的差別在哪裡?阻塞在何處?為什麼會有多種IO模型,分别用來解決問題?常用的架構采用的是何種I/O模型?各種IO模型的優劣勢在哪裡,适用于何種應用場景?
簡而言之,對于I/O的認知,不能僅僅停留在字面上認識,了解内部玄機,才能深刻了解I/O,才能看清I/O相關問題的本質。
2. I/O 的定義
I/O 的全稱是 Input/Output。雖常談及I/O,但想必你也一時不能給出一個完整的定義。搜尋了谷歌,發現也盡是些冗長的論述。要想厘清I/O這個概念,我們需要從不同的視角去了解它。
2.1 計算機視角
馮•諾伊曼計算機的基本思想中有提到計算機硬體組成應為五大部分:控制器,運算器,存儲器,輸入和輸出。其中輸入是指将資料輸入到計算機的裝置,比如鍵盤滑鼠;輸出是指從計算機中擷取資料的裝置,比如顯示器;以及既是輸入又是輸出裝置,硬碟,網卡等。
使用者通過作業系統才能完成對計算機的操作。計算機啟動時,第一個啟動的程式是作業系統的核心,它将負責計算機的資源管理和程序的排程。換句話說:作業系統負責從輸入裝置讀取資料并将資料寫入到輸出裝置。
是以I/O之于計算機,有兩層意思:
- I/O裝置
- 對I/O裝置的資料讀寫
對于一次I/O操作,必然涉及2個參與方,一個輸入端,一個輸出端,而又根據參與雙方的裝置類型,我們又可以分為磁盤I/O,網絡I/O(一次網絡的請求響應,網卡)等。
2.2 程式視角
應用程式作為一個檔案儲存在磁盤中,隻有加載到記憶體到成為一個程序才能運作。應用程式運作在計算機記憶體中,必然會涉及到資料交換,比如讀寫磁盤檔案,通路資料庫,調用遠端API等等。但我們編寫的程式并不能像作業系統核心一樣直接進行I/O操作。
因為為了確定作業系統的安全穩定運作,作業系統啟動後,将會開啟保護模式:将記憶體分為核心空間(核心對應程序所在記憶體空間)和使用者空間,進行記憶體隔離。我們建構的程式将運作在使用者空間,使用者空間無法操作核心空間,也就意味着使用者空間的程式不能直接通路由核心管理的I/O,比如:硬碟、網卡等。
但作業系統向外提供API,其由各種類型的系統調用(System Call)組成,以提供安全的通路控制。是以應用程式要想通路核心管理的I/O,必須通過調用核心提供的系統調用(system call)進行間接通路。
是以I/O之于應用程式來說,強調的通過向核心發起系統調用完成對I/O的間接通路。換句話說應用程式發起的一次IO操作實際包含兩個階段:
- IO調用階段:應用程式程序向核心發起系統調用
-
IO執行階段:核心執行IO操作并傳回
2.1 準備資料階段:核心等待I/O裝置準備好資料
2.2 拷貝資料階段:将資料從核心緩沖區拷貝到使用者空間緩沖區
怎麼了解準備資料階段呢?對于寫請求:等待系統調用的完整請求資料,并寫入核心緩沖區;對于讀請求:等待系統調用的完整請求資料;(若請求資料不存在于核心緩沖區)則将外圍裝置的資料讀入到核心緩沖區。
而應用程式程序在發起IO調用至核心執行IO傳回之前,應用程式程序/線程所處狀态,就是我們下面要讨論的第二個話題阻塞IO與非阻塞IO。
3. IO 模型之阻塞 I/O (BIO)
應用程式中程序在發起IO調用後至核心執行IO操作傳回結果之前,若發起系統調用的線程一直處于等待狀态,則此次IO操作為阻塞IO。阻塞IO簡稱BIO,Blocking IO。其處理流程如下圖所示:
從上圖可知當使用者程序發起IO系統調用後,核心從準備資料到拷貝資料到使用者空間的兩個階段期間使用者調用線程選擇阻塞等待資料傳回。
是以BIO帶來了一個問題:如果核心資料需要耗時很久才能準備好,那麼使用者程序将被阻塞,浪費性能。為了提升應用的性能,雖然可以通過多線程來提升性能,但線程的建立依然會借助系統調用,同時多線程會導緻頻繁的線程上下文的切換,同樣會影響性能。是以要想解決BIO帶來的問題,我們就得看到問題的本質,那就是阻塞二字。
4. IO 模型之非阻塞 I/O (NIO)
那解決方案自然也容易想到,将阻塞變為非阻塞,那就是使用者程序在發起系統調用時指定為非阻塞,核心接收到請求後,就會立即傳回,然後使用者程序通過輪詢的方式來拉取處理結果。也就是如下圖所示:
應用程式中程序在發起IO調用後至核心執行IO操作傳回結果之前,若發起系統調用的線程不會等待而是立即傳回,則此次IO操作為非阻塞IO模型。非阻塞IO簡稱NIO,Non-Blocking IO。
然而,非阻塞IO雖然相對于阻塞IO大幅提升了性能,但依舊不是完美的解決方案,其依然存在性能問題,也就是頻繁的輪詢導緻頻繁的系統調用,會耗費大量的CPU資源。比如當并發很高時,假設有1000個并發,那麼機關時間循環内将會有1000次系統調用去輪詢執行結果,而實際上可能隻有2個請求結果執行完畢,這就會有998次無效的系統調用,造成嚴重的性能浪費。有問題就要解決,那NIO問題的本質就是頻繁輪詢導緻的無效系統調用。
5. IO 模型之 IO 多路複用
解決NIO的思路就是降解無效的系統調用,如何降解呢?我們一起來看看以下幾種IO多路複用的解決思路。
5.1 IO多路複用之select/poll
Select是核心提供的系統調用,它支援一次查詢多個系統調用的可用狀态,當任意一個結果狀态可用時就會傳回,使用者程序再發起一次系統調用進行資料讀取。換句話說,就是NIO中N次的系統調用,借助Select,隻需要發起一次系統調用就夠了。其IO流程如下所示:
但是,select 有一個限制,就是存在連接配接數限制,針對于此,又提出了 poll。其與 select 相比,主要是解決了連接配接限制。
select/poll 雖然解決了 NIO 重複無效系統調用用的問題,但同時又引入了新的問題。問題是:
- 使用者空間和核心空間之間,大量的資料拷貝
- 核心循環周遊IO狀态,浪費CPU時間
換句話說,select/poll 雖然減少了使用者程序的發起的系統調用,但核心的工作量隻增不減。在高并發的情況下,核心的性能問題依舊。是以select/poll的問題本質是:核心存在無效的循環周遊。
5.2 IO 多路複用之 epoll
針對select/pool引入的問題,我們把解決問題的思路轉回到核心上,如何減少核心重複無效的循環周遊呢?變主動為被動,基于事件驅動來實作。其流程圖如下所示:
epoll 相較于 select/poll,多了兩次系統調用,其中 epollcreate 建立與核心的連接配接,epollctl 注冊事件,epoll_wait 阻塞使用者程序,等待 IO 事件。
epoll,已經大大優化了IO的執行效率,但在IO執行的第一階段:資料準備階段都還是被阻塞的。是以這是一個可以繼續優化的點。
6. IO 模型之信号驅動 IO (SIGIO)
信号驅動 IO 與 BIO 和 NIO 最大的差別就在于,在 IO 執行的資料準備階段,不會阻塞使用者程序。如下圖所示:當使用者程序需要等待資料的時候,會向核心發送一個信号,告訴核心我要什麼資料,然後使用者程序就繼續做别的事情去了,而當核心中的資料準備好之後,核心立馬發給使用者程序一個信号,說”資料準備好了,快來查收“,使用者程序收到信号之後,立馬調用 recvfrom,去查收資料。
乍一看,信号驅動式I/O模型有種異步操作的感覺,但是在IO執行的第二階段,也就是将資料從核心空間複制到使用者空間這個階段,使用者程序還是被阻塞的。
綜上,你會發現,不管是BIO還是NIO還是SIGIO,它們最終都會被阻塞在IO執行的第二階段。那如果能将IO執行的第二階段變成非阻塞,那就完美了。
7. IO 模型之異步 IO (AIO)
異步IO真正實作了IO全流程的非阻塞。使用者程序發出系統調用後立即傳回,核心等待資料準備完成,然後将資料拷貝到使用者程序緩沖區,然後發送信号告訴使用者程序IO操作執行完畢(與SIGIO相比,一個是發送信号告訴使用者程序資料準備完畢,一個是IO執行完畢)。其流程如下:
是以,之是以稱為異步IO,取決于IO執行的第二階段是否阻塞。是以前面講的BIO,NIO和SIGIO均為同步IO。
8. 總結
梳理完這些IO模型後,之前一直處于懵懂狀态的阻塞,非阻塞,同步異步IO,終于算是有個概念了。同時也糾正了自己一直以來的誤解,是以一路走來,愈發覺得返璞歸真的重要性,隻有如此,才能在快速更疊的技術演進中,以不變應萬變。
本文綜合多方資料寫就,難免纰漏,但隻有寫下來,才能得以指正。是以,煩請各位看官不吝賜教。
參考資料
[1] 程式員應該這樣了解IO
[2] IO複用模型同步,異步,阻塞,非阻塞及執行個體詳解
[3] 伺服器網絡程式設計之 IO 模型
[4] http://www.c-jump.com/CIS77/CPU/VonNeumann/lecture.html
[5] 同步I/O(阻塞I/O,非阻塞I/O),異步I/O
[6] 馬士兵:權威講解nio,epoll,多路複用
[7] Linux 核心詳解以及核心緩沖區技術