IO 的概念及 IO 五種模型,epoll 工作原理

IO 是什麼?

在 Linux 系統中一切皆文件,而文件指的就是一些二進制的流,這些流包括輸入流和輸出流,比如我們之前談到的,進程間通信,socket 套接字,管道,read 讀取數據,write 寫數據等等,在信息交換的過程中,我們都是對這些流進行操作,簡稱 IO 操作,那計算機裏面這些流,我們是通過文件描述符來進行操作的.

通常用戶進程的一個完整 IO 分爲兩個階段

從用戶空間到內核空間,從內核空間到設備空間

內核空間存放的是內核代碼和數據,用戶空間存放的是用戶程序代碼和數據,不管是內核還是用戶,他們都處於虛擬地址空間,Linux 使用兩級保護機制,0 級供內核使用,3 級供用戶程序使用,也就是說,用戶無法直接操作內核數據,必須通過系統調用請求內核完成 IO 操作

所以對於一個輸入操作來說,進程 IO 系統調用後,內核會先看緩衝區有沒有數據,如果沒有到設備中讀取,因爲設備 IO 速度比較慢,對於用戶來說,這是一個等待的過程,當內核緩衝區有數據則複製到進程的空間

所以 IO 操作的流程可以分爲兩步,等待 IO 的操作條件具備,然後進行數據拷貝

IO 一般有三種,內存 IO 、網絡 IO、磁盤 IO ,我們一般說的是後兩者

例子:網絡輸入操作
在這裏插入圖片描述

重要五種的 IO 模型

阻塞 IO : 在內核將數據準備好之前, 系統調用會一直阻塞等待,什麼事都不做,直到內核將數據準備好然後拷貝的用戶空間,返回一個成功的指示,這種方式效率很低

在這裏插入圖片描述
非阻塞 IO : 如果內核還未將數據準備好, 系統調用仍然會直接返回, 並且返回 EWOULDBLOCK 錯誤碼,在非阻塞 IO 中有一個弊端就是:輪詢讀寫文件描述符,這對於 CPU 來說是一種浪費

如下圖

信號驅動 IO : 首先在內核和進程之間會建立 SIGIO 的信號處理程序,當內核將數據準備好的時候, 使用 SIGIO 信號通知應用程序,這時候應用程序纔會系統調用,進行 IO 操作,如下圖

異步IO:由內核在數據拷貝完成後, 通知應用程序處理數據報(信號驅動 IO 是告訴應用程序何時可以開始拷貝數據).

IO多路轉接/IO多路複用: 雖然看起來和阻塞 IO 類似. 實際上核心在於 IO 多路轉接能夠同時等待多個文件描述符的就緒狀態.

IO 多路轉接是多了一個 select 函數,select 函數有一個參數是文件描述符集合,對這些文件描述符進行循環監聽,當某個文件描述符就緒時,就對這個文件描述符進行處理,其中,select 只負責等,recvfrom 只負責拷貝

IO 多路轉接是屬於阻塞 IO,但可以對多個文件描述符進行阻塞監聽,所以效率較阻塞 IO 的高

任何 IO 過程中, 都包含兩個步驟: 等待和拷貝. 在正常情況下等待的時間要比拷貝的時間長,所以提高效率最好的辦法就是減少等待的時間

同步通信和異步通信

這個同步和線程中的同步不是一個概念,這裏指的是:如果發出一個調用時,在沒有得到結果之前,該調用就不返回,換句話說,就是由調用者主動等待這個調用的結果

當一個異步過程調用發出後,直接返回,所以調用者不會立刻得到結果; 而是在調用發出後,被調用者通過狀態來通知調用者,或者通過回調函數處理這個調用

所以同步和異步的區別是請求發出後,是否需要等待結果,才能繼續執行其他操作,同步是需要等待,而異步不需要

阻塞和非阻塞

阻塞和非阻塞關注的是程序在等待調用結果時的狀態.

阻塞調用是指: 調用結果返回之前,當前線程會被掛起. 調用線程只有在得到結果之後纔會返回

非阻塞調用指: 在不能立刻得到結果之前,該調用不會阻塞當前線程

阻塞線程意思就是當前線程被掛起,讓出 CPU 資源,進入等待隊列,等待下一次被調度

參考文章:https://www.cnblogs.com/mhq-martin/p/9035640.html


I/O 多路轉接之 select

系統提供 select 函數用來實現多路複用輸入輸出模型,用戶進行 select 系統調用時,會進入阻塞狀態監視多個文件描述符的狀態

這個狀態有可讀,可寫,異常狀態

函數原型
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

第一個參數:需要監視的最大文件描述符+1

第二、三、四個參數:需要檢測的可讀,可寫,異常文件描述符集合

第五個參數:用來設置等待的時間,如果不設置則會一直等待,直到某個文件描述符發生事件.如果在特定的時間內沒有事件發生,則 select 返回 0

說明:select 第一個參數之所以要設置爲最大文件描述符+1,是因爲文件描述符從 0 開始,比如我們要檢測文件描述符4 ,5 , 6 需要檢測 7 次,也就是從0 到 6,否則 6 文件描述符沒有檢測到

select 特點、缺陷

可監控的文件描述符有上限,一般根據 fd_set 結構體大小決定的,比如 sizeof(fd_set) = 512 ,那麼上限就是 512 * 8,因爲 1 個字節佔 8 位,每一個 bit 位表示一個文件描述符

將 fd 文件描述符加入 select 集合後,還需要使用一個數組來保存這些文件描述符,以便 select 返回後清空未發生事件的文件描述符,同時可以遍歷出最大的文件描述符,計算出 select 第一個參數

每次調用 select 都要手動設置 fd 集合,比如你要監視讀文件描述符,則輸入讀的參數,這樣對用戶來說不太友好,因爲不方便

每次調用 select,都需要把文件描述符集合從用戶態拷貝到內核態,開銷太大,同時還需要遍歷文件描述符

I/O 多路轉接之 poll

在 select 的基礎上,接口變得比較簡單,採用事件結構的方式,將就緒文件描述符寫入一個結構體,但並沒有解決 select 效率低的問題

另外 poll 只能用於 Linux 下,無法跨平臺使用,select 具有移植性

I/O 多路轉接之 epoll (重要掌握)

是爲處理大批量句柄而作了改進的 poll

主要有三個核心 apl 和兩個數據結構(紅黑樹和鏈表)

在內核中創建 eventpoll 結構體

並返回一個文件描述符的操作句柄 ,size 決定 epoll 最多監控多少個描述符,默認忽略

int epoll_create(int size);

epoll 的事件註冊函數

int epoll_ctl (int epfd, int op, int fd, struct epoll_event* event)

將被監聽的文件描述符添加到紅黑樹結構中,或者對監聽的事件進行修改和刪除

epoll_ctl 對這棵樹進行管理,紅黑樹的每個成員由描述符值 data 和文件描述符指向的文件表項組成

第一個參數:epoll 句柄
第二個參數:表示動作,比如向紅黑數添加事件結構信息(EPOLL_CTL_ADD),從紅黑樹移除事件結構信息(EPOLL_CTL_DEL)、從紅黑樹修改事件結構信息(EPOLL_CTL_MOD)

第三個參數:需要監聽的 fd

第四個參數:告訴內核需要監聽什麼事,比如描述符可讀、可寫、異常狀態是被觸發

收集在 epoll 監控事件中已經發生的事件

int epoll_wait (int epfd, struct epoll_event * events, int maxevents, int timeout);

第一個參數是 epoll 的句柄

第二個參數表示,epoll 將會把觸發的事件寫入 events 數組,內核只負責寫入,但是不會分配內存

第三個參數 maxevents :返回的 events 的最大個數

第四個參數 timeout: 表示超時時間的毫秒(0 會立即返回,-1 是永久阻塞)

如果函數調用成功,返回對應 I/O 上已準備好的文件描述符數目,如返回 0 表示已超時, 返回小於 0 表示函數失敗

epoll 工作原理

當某一個進程調用 epoll_create 時,Linux 內核會創建一個 eventpoll 結構體,在這個結構體中包含兩個成員,一個是紅黑樹的根節點,一個是雙向鏈表節點

當調用註冊函數 epoll_ctl 的時候,會將用戶指定事件掛載到紅黑樹上,也就是說紅黑樹上添加着 epoll 對象中所有的事件,之所以選擇紅黑樹是因爲其可以高效識別重複添加的事件;所有添加到 epoll 中的事件都會與設備驅動建立回調關係,只有響應的事件被觸發,就會調用回調函數,把事件添加到雙向鏈表中;

對於每一個事件,都會建立一個 epitem 結構體,對這個事件進行描述,比如紅黑樹節點信息,雙向鏈表節點,期待發生的事件類型等

最後調用 epoll_wait 函數檢查是否有事件發生時,只需要檢查 event_poll 對象中的鏈表是否爲空,如果不爲空則把事件複製到用戶態,同時將事件的數量返回給用戶,操作複雜度爲O(1),如果爲空則進行等待.

epoll 和 select 、poll 比較

epoll 接口使用方便高效,不需要每次循環設置關注的文件描述符, 也做到了輸入輸出參數分離開,只需要一次將文件描述符拷貝到內存中,不像 select 每次循環都要進行拷貝

epoll 使用事件回調函數的機制,檢測文件描述符是否就緒,將就緒的文件描述符添加到就緒隊列中,epoll_wait 會直接訪問就緒隊列就知道哪些文件描述符就緒了,不需要向 select、poll 那樣進行遍歷,而且 epoll 對文件描述符的上限也沒有限制

在這裏插入圖片描述
當把文件描述符傳遞給用戶時,select、poll 還需要一次遍歷檢測哪些文件描述符就緒,對於 epoll 傳入給用戶的就是已經就緒的文件描述符,不需要經過遍歷

epoll 兩種觸發方式

水平觸發(LT)

當 epoll 檢測到 socket 上事件就緒的時候, 可以不立刻進行處理. 或者只處理一部分,這也是 epoll 默認的處理方式

比如我們把一個 tcp socket 添加到 epoll 對象中,這個時候 socket 另一端被寫入了 2k 的數據,當調用 epoll_wait 函數會返回,可能只會讀取 1k 的數據,剩下的 1k 數據下一次讀取時 epoll_wait 也會立刻返回,直到緩衝區數據被處理完,epoll_wait 就不會返回了

邊緣觸發(ET)

當 epoll 檢測到 socket 上事件就緒時, 必須立刻處理.如上面的例子, 雖然只讀了 1k 的數據, 緩衝區還剩 1k 的數據, 在第二次調用 epoll_wait 的時候 ,epoll_wait 不會再返回了,也就是說, 邊緣觸發的模式下, 文件描述符上的事件就緒後, 只有一次處理機會

對比 LT 和 ET

ET 的性能比 LT 性能更高,因爲 epoll_wait 返回的次數少了很多

select 和 poll 其實也是工作在 LT 模式下. epoll 既可以支持 LT, 也可以支持 ET,在第 1 步將 socket 添加到 epoll 描述符的時候使用EPOLLET 標誌,就會進入 ET 模式

Nginx 默認採用 ET 模式使用 epoll ,只支持非阻塞的讀寫,所以 ET 模式下的 epoll, 需要將文件描述設置爲非阻塞進行輪詢遍歷. 這個不是接口上的要求, 而是 “工程實踐” 上的要求

就像剛纔的例子,如果 ET 模式使用阻塞的方式,那麼第二次讀取 1k 數據時,epoll_wait 就不會返回,因爲只處理一次就緒事件,第二次只有在往進去寫數據,纔會觸發事件

epoll 場景

對於多連接,且多個連接中只有一部分連接比較活躍時選用 epoll 比較合適,比如一個需要處理上萬個客戶端的 app 入口服務器, 這樣的服務器就很適合 epoll

epoll 中的驚羣問題

在多進程或多線程的環境下,我們爲了提高程序的效率和穩定性,採用 epoll 的模式,讓多個進程或線程同時在 epoll_wait 函數監聽文件描述符,但當一個新的請求鏈接到來時,操作系統不知道喚醒哪個線程,就乾脆一起喚醒,但是隻有一個線程可以處理 accept 事件,其他線程都將失敗,這種現象稱爲驚羣效應,帶來的結果就是資源的消耗和性能的影響

如何解決這個問題?

這種情況,不建議讓多個線程同時在 epoll_wait 監聽的 socket,而是讓其中一個線程 epoll_wait 監聽的 socket , 當有新的鏈接請求進來之後,由 epoll_wait 的線程調用 accept,建立新的連接,然後交給其他工作線程處理後續的數據讀寫請求,這樣就可以避免了由於多線程環境下的 epoll_wait 驚羣效應問題

舉個栗子:就好比我們去星級酒店喫飯,門前一定有招待客人的服務生,但是它只是招待新到來的客戶,至於客戶進到酒店後,它只要把客戶交到酒店內部的服務生就可以了,沒必要自己去處理所有的事情

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