從網路I/O模型到Netty,先深入瞭解下I/O多路複用

阿丸發表於2021-02-09

微信搜尋【阿丸筆記】,關注Java/MySQL/中介軟體各系列原創實戰筆記,乾貨滿滿。

 

本文是Netty系列第3篇

上一篇文章我們瞭解了Unix標準的5種網路I/O模型,知道了它們的核心區別與各自的優缺點。尤其是I/O多路複用模型,在高併發場景下,有著非常好的優勢。而Netty也採用了I/O多路複用模型。

那Netty是如何實現I/O多路複用的呢?

Netty實際上也是一個封裝好的框架,它的本質上還是使用了Java的NIO包(New IO,不是網路I/O模型的NIO,Nonblocking IO)包,Java NIO包裡面使用了I/O多路複用。

所以,本文作為一個 前置知識 + 高頻面試題 章節(手動狗頭),一起來深入瞭解下I/O多路複用模型吧。

 

本文預計閱讀時間 5分鐘,將重點回答以下兩個問題:

  • I/O多路複用模式有哪些實現?select/poll/epoll
  • select/poll/epoll有什麼區別

1.I/O多路複用模式的實現

這是我們上一篇講I/O多路複用使用的圖,可以再回顧一下I/O多路複用模型。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

多個的程式的IO可以註冊到一個複用器(selector)上,然後用一個程式呼叫select,select會監聽所有註冊進來的IO。

舉個例子。
在BIO模式中,一個老師(應用程式/執行緒)只能同時處理一個同學(IO流)的問題。如果有10個同學,就需要配置10個老師來做一對一的講解。

在IO多路複用模型中。我們給 老師 配置了一個 班長(複用器Selector)。班長 負責觀察班級裡的10個同學誰要提問,一旦有同學舉手,班長就反饋老師去處理這個舉手同學的問題。

這樣一來,只需要1個老師,老師 只需要注意 班長 的反饋,就能及時處理對應的 同學 的問題了。

下面我們具體來看看I/O多路複用的三種實現:select、poll、epoll。

需要注意的是,select,poll,epoll都是IO多路複用的實現方式,而且本質上都是同步I/O,因為它們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的。
而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

2. select

Linux系統提供了一個函式select來供開發者使用select多路複用機制。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

該函式的作用是:

通過輪詢,可以同時監視多個檔案描述符是否發生了讀、寫、異常這三類IO事件。

最後返回發生IO事件的檔案描述符數量,以及讀事件、寫事件、異常事件這三種事件分別發生在哪些檔案描述符中(readfds、writefds、errorfds三個引數)。

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

Linux下一切皆檔案,包括IO裝置也是。因此要對某個裝置進行操作,就需要開啟此裝置檔案,開啟檔案就會獲得該檔案的檔案描述符fd( file discriptor),它就是一個很小的整數。

 

我們結合 老師-班長-同學 的模型來理解下這個過程。

  • 老師把學生名單(xxxxfds)給班長,讓班長關注班級裡的所有同學。
  • 班長時刻輪訓班級裡每個同學的狀態(輪訓所有fd_set),直到 超時 或者 有同學舉手。
  • 一旦有同學舉手,班長就會把學生名單上有變化的學生名字做標記,並把一共多少個學生有變化返回給 老師。
  • 老師可以獲得舉手同學的數量,並在學生名單(xxxxfds)上看的有哪幾個同學發生了事件(讀、寫、異常)。
  • 老師拿到學生名單後,輪訓班級裡面的每個同學狀態,根據具體的 讀、寫、異常事件 來進行IO處理。

特別注意,在select函式下,老師僅僅知道有學生髮生變化了,但到底是哪些學生髮生變化,他需要 輪訓 一遍同學名單,找出舉手的同學,然後傾聽他的問題,並回答他的問題。

select的缺點比較明顯:

  • 具有O(n)的無差別輪詢時間複雜度,每次呼叫需要輪訓fd_set,同時處理得越多,輪詢時間就越長。
  • 每次呼叫select函式,都需要把 所有 fd_set從 使用者態 拷貝到 核心態 進行輪訓,如果fd_set比較大,對效能影響就非常大。

3. poll

poll的實現和select非常相似,我們就不重複說明了,直接介紹一下區別。poll函式如下:

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

主要是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,pollfd結構使用連結串列而非陣列,這導致pollfd的長度沒有限制。但是如果pollfd長度過大,會導致效能下降。

除此之外,二者的原理基本一致,即對多個描述符也是進行輪詢,根據描述符的狀態進行處理。

因此,二者的缺陷也基本一致。

4. epoll

epoll的全稱是eventpoll,它是基於event事件進行實現的,是linux特有的I/O複用函式。

它在實現和使用上和select\poll有很大差別:

  • epoll通過一組函式來完成任務,而不是單個函式。
  • epoll把使用者關心的檔案描述符fd放在一個事件表中,而不是像select/poll那樣把所有檔案描述符集合(fds)傳來傳去。
  • epoll需要一個額外的檔案描述符fd來表示這個事件表。

不同於select使用三個fd_set來對應讀/寫/異常的IO變化,epoll專門定義了一個epoll_event結構體,將其作為讀/寫/異常的IO變化的邏輯封裝,稱為事件(event)。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

4.1 epoll的三個核心函式

epoll把原先的select/poll呼叫分成了3個函式。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

  • 呼叫int epoll_create(int size)建立一個epoll控制程式碼物件,返回一個檔案描述符fd,指向 事件表。在linux下如果檢視/proc/程式id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。
  • 引數size並不是限制了epoll所能監聽的描述符最大個數,只是對核心初始分配內部資料結構的一個建議。
從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

  • 呼叫epoll_ctl向epoll物件中新增連線的套接字。
  • epfd就是epoll_creat返回的id。
  • op表示具體操作。包括新增fd的監聽事件EPOLL_CTL_ADD、刪除fd的監聽事件EPOLL_CTL_DEL、修改fd的監聽事件EPOLL_CTL_MOD。
  • fd是需要監聽的fd(檔案描述符)
  • event是告訴核心需要監聽哪個事件
從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

  • 呼叫epoll_wait收集發生的事件的連線
  • 返回值表示已經準備繼續的檔案描述符的總數。
  • epfd表示事件表。
  • events表示 準備就緒的事件陣列。event_wait如果檢測到事件,就把就緒的事件從 事件表 中複製到這個陣列中。(比select/poll高效的地方!!)
  • maxevents表示最多監聽多少事件。

4.2 epoll的實現原理

當某一程式呼叫 epoll_create()方法 時,核心空間會建立一個eventpoll結構體,這個結構體中有兩個成員變數與epoll的使用方式密切相關,結構體如下所示:

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

  • 紅黑樹根節點rbr:紅黑樹的根節點,這顆樹中儲存著所有新增到epoll中的需要監控的事件
  • 連結串列rdlist:連結串列中則存放著將要通過epoll_wait返回給使用者的滿足條件的事件

用 epoll_ctl()方法 將新新增的監控事件event加入到 紅黑樹rbr 中。還會給核心中斷處理程式註冊一個 回撥函式,告訴核心,如果這個控制程式碼的中斷到了,就把它放到準備就緒list連結串列裡。

一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,被觸發的事件會被 回撥函式 加入eventpoll的 連結串列rdlist 中。

當呼叫 epoll_wait()方法 檢查是否有事件發生時,只需要檢查eventpoll物件中的rdlist連結串列中是否有元素即可。如果連結串列中有資料的話,就把對應有修改的事件event複製到epoll_wait()方法的events陣列變數中,使用者就能獲得了。

對比select/poll,我們可以看到此處不需要遍歷監聽的檔案描述符,這正是epoll的魅力所在。

如此一來,epoll_wait的效率就非常高了。因為呼叫epoll_wait時,不需要向作業系統複製所有的連線的控制程式碼資料,核心也不需要去遍歷全部的連線。

4.3 epoll中有使用共享記憶體嗎?

很多部落格提到了這點:

epoll_wait返回時,對於就緒的事件,epoll使用的是共享記憶體的方式,即使用者態和核心態都指向了就緒連結串列,所以就避免了記憶體拷貝消耗

但是事實確實如此嗎?

原始碼面前無密碼,我們直接看下原始碼吧。
參考eventpoll.c的原始碼。

https://github.com/torvalds/linux/blob/master/fs/eventpoll.c

具體的epoll_wait呼叫關係如下圖所示。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

我們可以在put_user中看到具體的說明。

從網路I/O模型到Netty,先深入瞭解下I/O多路複用

 

因此,事件確實是從核心空間拷貝到使用者空間的,並沒有使用共享記憶體。

5.三種實現對比

通過上面的分析,相信大家都已經瞭解了select/poll/epoll的實現。
下面通過一個表格來總結他們的主要區別。

 

select

poll

epoll

事件集合

使用者通過傳遞3個引數來關注 可讀、可寫、異常 3類事件

統一所有事件型別,只用1個引數來關注

通過1個事件表來管理使用者訂閱的事件

事件傳遞效率

每次等待socket事件,都需要把所有socket從使用者態拷貝至核心態

同select

只需將socket新增一次到紅黑樹上即可

核心檢測就緒效率

每次呼叫需要掃描整個註冊的檔案描述符集合,然後將其中已經就緒的檔案描述符設定在傳入的陣列中

同select

通過回撥機制,將就緒的事件加入連結串列rdlist

應用檢查已經就緒的 事件 效率

遍歷,O(n)複雜度

遍歷, O(n)複雜度

只獲取已經就緒的事件rdlist,O(1)複雜度

最大支援檔案描述符

陣列存放事件,有最大限制

連結串列存放事件,無最大限制(受系統最大限制)

紅黑樹存放事件,無最大限制(受系統最大限制)

從整體來看,epoll的實現效能是比select/poll更好的。

當然,如果保持活躍的連線一直非常多,epoll_wait的效率就不一定高了,因為此時epoll_wait的回撥函式觸發過於頻繁。

因此,epoll最適合的場景是連線數量很多,但是活躍連線數量不多的情況。

 


參考書目:
《Linux高效能伺服器程式設計》

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章