IO多路復用


划分內核態/用戶態

之前說過七層/五層/四層的網絡模型,我們從網絡模型可以看出傳輸層(tcp/udp)開始 就是我們平常編寫程序運行的層次了。在系統層級,為了系統安全之類的考慮我們將 傳輸層向上 划分為用戶態傳輸層向下 划分到 內核態(暫時可以認為這么划分)
image.png

客戶端-服務端

在網絡交互中客戶端服務端的交互時發生了什么?

  1. 首先我們應用啟動運行,對外暴露一個端口(或者多個),此時調用系統``創建一個(或多個)這個端口的socket(或者說是創建一個(或多個)監聽器)
  2. 客戶端發起請求,此時客戶端生成一個socket 然后通過 傳輸層->網絡層->鏈路層->物理層 ->( 物理層-鏈路層-網絡層-傳輸層)``服務器 進行三次握手確認鏈接
  3. 然后客戶端數據按照第二部的鏈路順序 在發送到 服務端
  4. 服務端網卡->將數據(0/1)讀出 -> 內核態 -> 用戶態
  5. 用戶態處理數據,將處理后的數據再原路返回。

image.png
從上我們可以知道 客戶端服務端數據``流向。這僅僅是一台客戶端的,作為服務器肯定是要有多台客戶端進行通信的,如果有多個客戶端同時訪問此時的過程如何呢?這就引出我們今天要說的主題:IO多路復用。為了講清楚,我們先將傳統的網絡io拉出來進行一步步推導。

我們在上面說過,服務端應用啟動的時候會創建一個主動socket(也就是監聽器),那么如果有客戶端建立鏈接的時候被監聽到,然后執行 創建一個被動socket執行服務端的代碼:服務端一般就是讀取數據 然后 處理數據 最后返回數據 關閉鏈接
但是 我們建立鏈接的時候 數據還沒有``到達``用戶態,也就是此時數據不一定傳輸完成了。那么我們服務端的讀數據 也就被阻塞了(我們程序發起io調用,如果內核態 沒有准備好,那么我們程序是在io 階段被阻塞的,也就是我們平常說的系統卡了)。此時就引出我們第一個概念:阻塞IO

關於數據的阻塞

image.png
image.png

image.png

Read 過程

在上面我們可以看到 客戶端寫入流 和 服務端讀入流 是有一個阻塞的階段的(客戶端可以分多次寫入流,然后發送到服務端),而且這里我們要注意的是,這個流是從物理層傳入的(服務端舉例子,客戶端是相反的),那么數據到達用戶層 還是有一個 內核態用戶態切換(這個上下文切換是比較耗費性能的)。
然后我們對從 底層 到 用戶層的過程進行一下分析:將read (系統提供的read 函數)展開來,可以發現這個read 分成兩個部分:
數據從外部流入網卡然后走到內核緩沖區,此時客戶端socke文件描述符變成1,然后用戶緩沖區再去讀取(服務端進行讀取使用)
image.png

image.png

一、阻塞IO

在這個模型中,服務端處理請求是串聯的。也就是說如果這個請求被阻塞了,那么剩下的請求都要被阻塞``等待``上一個請求處理完成才行。所以,我們上面說,在 服務器讀數據的時候,數據還沒到(數據還沒讀到用戶態),那么服務器被阻塞,然后其他客戶端的請求也不能被處理。

比如:
小明和小紅兩個人訪問同一個服務,然后小明先點,但是數據沒被處理完成,然后小紅在進行發送請求,此時服務器就將小紅的請求掛起,等待小明的處理完成在進行處理。

這樣來說,服務器的cpu豈不是會浪費?當用戶數量少的時候還可以,但是如果用戶數量多來怎么行
所以我們就自己優化一下。

優化阻塞io

怎么優化,既然服務器此時還在等待數據,那么我們在開一個線程去處理另外的客戶端不就ok了?
所以我們對監聽read進行解耦合,監聽到一個客戶端就放進來一個客戶端的請求,然后服務再啟動一個線程去處理這個請求。
但是這個有兩個比較突出的問題:

  1. 服務端需要開辟大量線程,這對服務端的壓力是很大的
  2. 這個read 還是單線程``阻塞的,我們沒辦法向下走啊

所以這個對於傳統的io 來說還是沒有解決實際的問題,想要解決只能在操作系統中(內核態)處理。而這就引出我們第二個概念:非阻塞io

二、非阻塞IO

我們在看上面的read 函數,可以發現read 函數是分成兩個部分進行的,那我們是不是可以將這個兩個過程分開?
服務端的read 執行,然后read 直接返回-1 讓 服務端 代碼進行下一步操作,不用在阻塞到讀取這里。
image.png
雖然系統不再阻塞服務端的讀取程序了,但是服務端還是要使用這個數據啊,所以服務端還是需要有個線程不斷的進行循環,以此知道數據讀取完成了,所以還是有服務端創建線程的壓力啊。(也就是我們還是需要自己循環這個狀態;還有一點 read 讀取 還是 阻塞的,我們非阻塞的只是數據預處理階段-也就是網卡到內核緩沖區的部分,這個是同步異步的一個重要區分點)
image.png

優化

我們再一次發揮聰明的頭腦,既然服務端為每個客戶端創建一個線程是耗費創建線程的壓力,那么就將每個客戶端的文件描述符存儲起來(數組),然后等到可以在用戶態read 的時候在調用服務端的注冊函數不就ok了,然后單獨創建一個線程 專門用來做 遍歷。這樣不就減少了服務端的壓力了
image.png
image.png
這是不是有點多路復用的意思?
但是我們在應用層寫的read 還是要調用系統的read 方法,也就是還是需要消耗系統資源的(在 while 循環里做系統調用,就好比你做分布式項目時在 while 里做 rpc 請求一樣,是不划算的)。所以我們能不能扔到系統中去?這就引出我們今天的角-io多路復用

三、IO多路復用

多路復用的思想: 是 在 非阻塞 io 的基礎上進行優化的,也就是對於 read 第一部分 預處理階段非阻塞的。(可以理解為,我們告訴系統那些在等待,等系統處理好了 在通知 系統,我們再去調用io 讀取)

select

此時操作系統提供了一個select 函數,我們可以通過它把一個文件描述符的數組發給操作系統, 讓操作系統去遍歷,確定哪個文件描述符可以讀寫, 然后告訴我們去處理:

image.png
這里注意一下,雖然我們讓系統遍歷了,但是我們自己還是需要遍歷的,只不過此時我們自己遍歷的沒有了系統的開銷了。然后有了數據之后我們在進行調用注冊函數。

image.png

但是我們知道

  1. 系統調用fd 數據,也就是拷貝一份到內核,高並發場景下這樣的拷貝消耗的資源是驚人的。(可優化為不復制)且數組也是有限制的。
  2. 還有select 在內核層仍然是通過遍歷的方式檢查文件描述符的就緒狀態,是個同步過程,只不過無系統調用切換上下文的開銷。(內核層可優化為異步事件通知)
  3. select 僅僅返回可讀文件描述符的個數,具體哪個可讀還是要用戶自己遍歷。(可優化為只返回給用戶就緒的文件描述符,無需用戶做無效的遍歷)

這一點我們還有一個注意的點:我們read第二步還是在阻塞

poll

為了解決數組的限制(這不阻礙高並發的數量么),所以它用了動態數組,也就是鏈表,去掉了 select 只能監聽 1024 個文件描述符的限制。

epoll

此時我們的終極解決方案過來了
epoll 主要就是針對這三點進行了改進。
image.png

  1. 內核中保存一份文件描述符集合,無需用戶每次都重新傳入,只需告訴內核修改的部分即可。
  2. 內核不再通過輪詢的方式找到就緒的文件描述符,而是通過異步 IO 事件喚醒。
  3. 內核僅會將有 IO 事件的文件描述符返回給用戶,用戶也無需遍歷整個文件描述符集合。

這里我們就將linux中的io 多路復用講完了。
image.png

四、信號驅動IO

我們在io多路復用,到最后我們的epoll 中,可以看到,最后是內核態 將准備好的io 給到 應用層的 程序,所以我們可以進一步來進行一下優化,我們在程序層 將 數據准備 和io讀取 進行分開:
也就是 在主線中調用 數據預處理 等方法,然后另寫 一個方法對 預處理完成 之后的方法進行 處理。也就是在程序層我們做一個“異步” (注意 ,這里其實還是同步的,因為我們的read 第二部分還是阻塞的,也就是我們還是在等待這個read ,可以理解為:我們不在主線程等待了,對於內核態來說並不知道,認為 用戶態的這段還是在一個線程中)

五、異步IO(AIO)

我們上面做了那么多, 我們在應用層做的都是想要 在內核態數據真正 讀取到用戶態 的時候才使用數據,所以 我們考慮一下系統 對於第二部分也進行一個非阻塞的 返回 不就ok 了。
也就是 服務端(用戶態)進行一次系統調用(一次上下文切換),然后就往下進行,然后內核態 完成 用戶態的 拷貝的時候在進行通知,處理。

總結

注意一下,本章重點想要說的是 io 多路復用,其他都是用來和 多路復用進行輔助理解的。

  1. 阻塞io 就是 服務端 從建立鏈接 ->讀取數據->處理數據 都是一個線程中完成,一次只處理一個;
  2. 我們通過 創建多線程 來解決 防止 主線程 卡主 或者其他線程等待的時間太長的問題
  3. 非阻塞io 出來之后,我們就可以將 監聽 和 讀取 在操作系統層面解 耦合,但是我們還是需要自己遍歷狀態
  4. select 函數出來之后,我們可以將數組放到用戶態進行處理(還是需要自己遍歷,只不過沒有系統開銷了)
  5. poll 使用動態數組來儲存 描述符,解決數組長度問題(還是需要自己遍歷,只不過沒有系統開銷了)
  6. epoll 不用每次都傳入 描述符,然后使用紅黑樹 提高系統的性能,這下 我們應用層 終於不用在寫遍歷去處理了。
  7. 型號驅動io 是 程序層結偶,等待 內核台可以讀取的時候,在進行io 調用
  8. 異步io(AIO) 是 內核態 進行解耦 ,也就是我們程序層 一次調用,然后內核態 到用戶層的拷貝 完成的時候 我們的程序層的io 調用就被執行了,不用再去程序層 另外寫東西執行了。

image.png
最后說一下,這些都是我自己的理解,如果內容有誤,請聯系告知,在此不勝感激!!!

本文的內容都是使用下面的博客進行理解自己修改的:
https://baijiahao.baidu.com/s?id=1718409483059542510&wfr=spider&for=pc
https://zhuanlan.zhihu.com/p/470778284
部分圖片來源百度搜索
如有侵權請聯系我刪除,感謝!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM