IO 模型知多少 (1)

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操作實際包含兩個階段:

  1. IO調用階段:應用程序進程向內核發起系統調用
  2. 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 內核詳解以及內核緩衝區技術

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章