微信搜索【阿丸筆記】,關注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多路復用模型。

多個的進程的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多路復用機制。

該函數的作用是:
通過輪詢,可以同時監視多個文件描述符是否發生了讀、寫、異常這三類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函數如下:

主要是描述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)。

4.1 epoll的三個核心函數
epoll把原先的select/poll調用分成了3個函數。

- 調用int epoll_create(int size)建立一個epoll句柄對象,返回一個文件描述符fd,指向 事件表。在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
- 參數size並不是限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。

- 調用epoll_ctl向epoll對象中添加連接的套接字。
- epfd就是epoll_creat返回的id。
- op表示具體操作。包括添加fd的監聽事件EPOLL_CTL_ADD、刪除fd的監聽事件EPOLL_CTL_DEL、修改fd的監聽事件EPOLL_CTL_MOD。
- fd是需要監聽的fd(文件描述符)
- event是告訴內核需要監聽哪個事件

- 調用epoll_wait收集發生的事件的連接
- 返回值表示已經准備繼續的文件描述符的總數。
- epfd表示事件表。
- events表示 准備就緒的事件數組。event_wait如果檢測到事件,就把就緒的事件從 事件表 中復制到這個數組中。(比select/poll高效的地方!!)
- maxevents表示最多監聽多少事件。
4.2 epoll的實現原理
當某一進程調用 epoll_create()方法 時,內核空間會創建一個eventpoll結構體,這個結構體中有兩個成員變量與epoll的使用方式密切相關,結構體如下所示:

- 紅黑樹根節點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調用關系如下圖所示。

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

因此,事件確實是從內核空間拷貝到用戶空間的,並沒有使用共享內存。
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…(歷史文章查閱非常方便)