深入聊聊 Linux 五種 IO 模型

一、相關概念講解

1、同步與異步

同步就是一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成後,依賴的任務才能算完成,這是一種可靠的任務序列。要麼成功都成功,失敗都失敗,兩個任務的狀態可以保持一致。

異步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什麼工作,依賴的任務也立即執行,只要自己完成了整個任務就算完成了。至於被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。

2、堵塞與非堵塞

阻塞和非阻塞這兩個概念與程序(線程)等待消息通知 (無所謂同步或者異步) 時的狀態有關。也就是說阻塞與非阻塞主要是程序(線程)等待消息通知時的狀態角度來說的。

阻塞調用是指調用結果返回之前,當前線程會被掛起,一直處於等待消息通知,不能夠執行其他業務。函數只有在得到結果之後纔會返回。

非阻塞和阻塞的概念相對應,指在不能立刻得到結果之前,該函數不會阻塞當前線程,而會立刻返回。雖然表面上看非阻塞的方式可以明顯的提高 CPU 的利用率,但是也帶了另外一種後果就是系統的線程切換增加。增加的 CPU 執行時間能不能補償系統的切換成本需要好好評估。

(a) 如果這個線程在等待當前函數返回時,仍在執行其他消息處理,那這種情況就叫做同步非阻塞;

(b) 如果這個線程在等待當前函數返回時,沒有執行其他消息處理,而是處於掛起等待狀態,那這種情況就叫做同步阻塞

同步 / 異步關注的是消息通知的機制,而阻塞 / 非阻塞關注的是程序(線程)等待消息通知時的狀態

3、用戶空間與內核空間

現在操作系統都是採用虛擬存儲器,那麼對 32 位操作系統而言,它的尋址空間(虛擬存儲空間)爲 4G(2 的 32 次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。爲了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操作系統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對 linux 操作系統而言,將最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF),供各個進程使用,稱爲用戶空間。

4、進程切換

爲了控制進程的執行,內核必須有能力掛起正在 CPU 上運行的進程,並恢復以前掛起的某個進程的執行。這種行爲被稱爲進程切換。因此可以說,任何進程都是在操作系統內核的支持下運行的,是與內核緊密相關的。

從一個進程的運行轉到另一個進程上運行,這個過程中經過下面這些變化:

1、保存處理機上下文,包括程序計數器和其他寄存器。

2、更新 PCB 信息。

3、把進程的 PCB 移入相應的隊列,如就緒、在某事件阻塞等隊列。

4、選擇另一個進程執行,並更新其 PCB。

5、更新內存管理的數據結構。

6、恢復處理機上下文。

注:總而言之就是很耗資源

5、進程的堵塞

正在執行的進程,由於期待的某些事件未發生,如請求系統資源失敗、等待某種操作的完成、新數據尚未到達或無新工作做等,則由系統自動執行阻塞原語 (Block),使自己由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也因此只有處於運行態的進程(獲得 CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用 CPU 資源的

6、文件描述符

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值指向內核爲每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者創建一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞着文件描述符展開。但是文件描述符這一概念往往只適用於 UNIX、Linux 這樣的操作系統。

7、緩存

緩存 IO 又被稱作標準 IO,大多數文件系統的默認 IO 操作都是緩存 IO。在 Linux 的緩存 IO 機制中,操作系統會將 IO 的數據緩存在文件系統的頁緩存(page cache )中,也就是說,數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。

緩存 IO 的缺點:

數據在傳輸過程中需要在應用程序地址空間和內核進行多次數據拷貝操作,這些數據拷貝操作所帶來的 CPU 以及內存開銷是非常大的。

二、IO 模型

網絡 IO 的本質是 socket 的讀取,socket 在 linux 系統被抽象爲流,IO 可以理解爲對流的操作。剛纔說了,對於一次 IO 訪問(以 read 舉例),數據會先被拷貝到操作系統內核的緩衝區中,然後纔會從操作系統內核的緩衝區拷貝到應用程序的地址空間。

所以說,當一個 read 操作發生時,它會經歷兩個階段:

第一階段:等待數據準備 (Waiting for the data to be ready)。

第二階段:將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)。

對於 socket 流而言,

第一步:通常涉及等待網絡上的數據分組到達,然後被複制到內核的某個緩衝區。

第二步:把數據從內核緩衝區複製到應用進程緩衝區。

網絡應用需要處理的無非就是兩大類問題,網絡 IO,數據計算。相對於後者,網絡 IO 的延遲,給應用帶來的性能瓶頸大於後者。

網絡 IO 的模型大致有如下幾種:

· 同步模型(synchronous IO)

· 阻塞 IO(bloking IO)

· 非阻塞 IO(non-blocking IO)

· 多路複用 IO(multiplexing IO)

· 信號驅動式 IO(signal-driven IO)

· 異步 IO(asynchronous IO)注:由於 signal driven IO 在實際中並不常用,所以我這隻提及剩下的四種 IO Model。

1、堵塞 IO 模型

應用程序調用一個 IO 函數,導致應用程序阻塞,等待數據準備好。 如果數據沒有準備好,一直等待… 數據準備好了,從內核拷貝到用戶空間,IO 函數返回成功指示。

當調用 recv() 函數時,系統首先查是否有準備好的數據。如果數據沒有準備好,那麼系統就處於等待狀態。當數據準備好後,將數據從系統緩衝區複製到用戶空間,然後該函數返回。在套接應用程序中,當調用 recv() 函數時,未必用戶空間就已經存在數據,那麼此時 recv() 函數就會處於等待狀態。

2、非堵塞 IO 模型

 

我們把一個 SOCKET 接口設置爲非阻塞就是告訴內核,當所請求的 I/O 操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的 I/O 操作函數將不斷的測試數據是否已經準備好,如果沒有準備好,繼續測試,直到數據準備好爲止。在這個不斷測試的過程中,會大量的佔用 CPU 的時間。上述模型絕不被推薦。

3、IO 複用模型

 

由於同步非阻塞方式需要不斷主動輪詢,輪詢佔據了很大一部分過程,輪詢會消耗大量的 CPU 時間,而 “後臺” 可能有多個任務在同時進行,人們就想到了循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它。如果輪詢不是進程的用戶態,而是有人幫忙就好了。那麼這就是所謂的 “IO 多路複用”

IO 多路複用有兩個特別的系統調用 select、poll、epoll 函數。select 調用是內核級別的,select 輪詢相對非阻塞的輪詢的區別在於 --- 前者可以等待多個 socket,能實現同時對多個 IO 端口進行監聽,當其中任何一個 socket 的數據準好了,就能返回進行可讀,然後進程再進行 recvform 系統調用,將數據由內核拷貝到用戶進程,當然這個過程是阻塞的。select 或 poll 調用之後,會阻塞進程,與 blocking IO 阻塞不同在於,此時的 select 不是等到 socket 數據全部到達再處理, 而是有了一部分數據就會調用用戶進程來處理。如何知道有一部分數據到達了呢?監視的事情交給了內核,內核負責數據到達的處理。也可以理解爲 " 非阻塞 " 吧。

I/O 複用模型會用到 select、poll、epoll 函數,這幾個函數也會使進程阻塞,但是和阻塞 I/O 所不同的的,這兩個函數可以同時阻塞多個 I/O 操作。而且可以同時對多個讀操作,多個寫操作的 I/O 函數進行檢測,直到有數據可讀或可寫時(注意不是全部數據可讀或可寫),才真正調用 I/O 操作函數。

對於多路複用,也就是輪詢多個 socket。多路複用既然可以處理多個 IO,也就帶來了新的問題,多個 IO 之間的順序變得不確定了,當然也可以針對不同的編號。

在 I/O 編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者 I/O 多路複用技術進行處理。I/O 多路複用技術通過把多個 I/O 的阻塞複用到同一個 select 的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程 / 多進程模型比,I/O 多路複用的最大優勢是系統開銷小,系統不需要創建新的額外進程或者線程,也不需要維護這些進程和線程的運行,降底了系統的維護工作量,節省了系統資源,I/O 多路複用的主要應用場景如下:

1、服務器需要同時處理多個處於監聽狀態或者多個連接狀態的套接字。

2、服務器需要同時處理多種網絡協議的套接字。

此時你是不是想到的了 redis 如何做的啊,redis 用的就是多路複用。

3、信號驅動 IO

 

簡介:兩次調用,兩次返回;

首先我們允許套接口進行信號驅動 I/O, 並安裝一個信號處理函數,進程繼續運行並不阻塞。當數據準備好時,進程會收到一個 SIGIO 信號,可以在信號處理函數中調用 I/O 操作函數處理數據。

4、異步 IO 模型

相對於同步 IO,異步 IO 不是順序執行。用戶進程進行 aio_read 系統調用之後,無論內核數據是否準備好,都會直接返回給用戶進程,然後用戶態進程可以去做別的事情。等到 socket 數據準備好了,內核直接複製數據給進程,然後從內核向進程發送通知。IO 兩個階段,進程都是非阻塞的。

Linux 提供了 AIO 庫函數實現異步,但是用的很少。目前有很多開源的異步 IO 庫,例如 libevent、libev、libuv。

 

5、5 種 I/O 模型的比較

不同 I/O 模型的區別,其實主要在等待數據和數據複製這兩個時間段不同,圖形中已經表示得很清楚了。

通過上面的圖片,可以發現 non-blocking IO 和 asynchronous IO 的區別還是很明顯的。在 non-blocking IO 中,雖然進程大部分時間都不會被 block,但是它仍然要求進程去主動的 check,並且當數據準備完成以後,也需要進程主動的再次調用 recvfrom 來將數據拷貝到用戶內存。而 asynchronous IO 則完全不同。它就像是用戶進程將整個 IO 操作交給了他人(kernel)完成,然後他人做完後發信號通知。在此期間,用戶進程不需要去檢查 IO 操作的狀態,也不需要主動的去拷貝數據。

 

同步非阻塞方式相比同步阻塞方式:

**優點:**能夠在等待任務完成的時間裏幹其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在同時執行)。

**缺點:**任務完成的響應延遲增大了,因爲每過一段時間纔去輪詢一次 read 操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數據吞吐量的降低。

三、select 、poll 、epoll 的區別?

1、支持一個進程所能打開的最大連接數

2、FD (文件描述符)劇增後帶來的 IO 效率問題

 

3、消息傳遞方式

 

綜上,在選擇 select,poll,epoll 時要根據具體的使用場合以及這三種方式的自身特點。

1、表面上看 epoll 的性能最好,但是在連接數少並且連接都十分活躍的情況下,select 和 poll 的性能可能比 epoll 好,畢竟 epoll 的通知機制需要很多函數回調。

2、select 低效是因爲每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善

補充知識點:

Level_triggered(水平觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait() 會通知處理程序去讀寫。如果這次沒有把數據一次性全部讀寫完 (如讀寫緩衝區太小),那麼下次調用 epoll_wait() 時,它還會通知你在上沒讀寫完的文件描述符上繼續讀寫,當然如果你一直不去讀寫,它會一直通知你!如果系統中有大量你不需要讀寫的就緒文件描述符,而它們每次都會返回,這樣會大大降低處理程序檢索自己關心的就緒文件描述符的效率!

Edge_triggered(邊緣觸發):當被監控的文件描述符上有可讀寫事件發生時,epoll_wait() 會通知處理程序去讀寫。如果這次沒有把數據全部讀寫完 (如讀寫緩衝區太小),那麼下次調用 epoll_wait() 時,它不會通知你,也就是它只會通知你一次,直到該文件描述符上出現第二次可讀寫事件纔會通知你!這種模式比水平觸發效率高,系統不會充斥大量你不關心的就緒文件描述符!

select(),poll() 模型都是水平觸發模式,信號驅動 IO 是邊緣觸發模式,epoll() 模型即支持水平觸發,也支持邊緣觸發,默認是水平觸發。

架構師技術聯盟 分享項目實踐,洞察前沿架構,聚焦雲計算、微服務、大數據、超融合、軟件定義、數據保護、人工智能、行業實踐和解決方案。

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