深刻理解 epoll

1 簡介

Epoll 是個很老的知識點,是後端工程師的經典必修課。這種知識具有的特色就是研究的人多,因此研究的趨勢就會愈來愈深。固然分享的人也多,因爲分享者水平良莠不齊,也產生的大量錯誤理解。java

今天我再次分享 epoll,確定不會列個表格,對比一下差別,那就太無聊了。我將從線程阻塞的原理,中斷優化,網卡處理數據過程出發,深刻的介紹 epoll 背後的原理,最後還會 diss 一些流行的觀點。相信不管你是否已經熟悉 epoll,本文都會對你有價值。linux

2 引言

正文開始前,先問你們幾個問題。程序員

一、epoll 性能到底有多高。不少文章介紹 epoll 能夠輕鬆處理幾十萬個鏈接。而傳統 IO 只能處理幾百個鏈接 是否是說 epoll 的性能就是傳統 IO 的千倍呢?後端

二、不少文章把網絡 IO 劃分爲阻塞,非阻塞,同步,異步。並表示:非阻塞的性能比阻塞性能好,異步的性能比同步性能好。設計模式

  • 若是說阻塞致使性能低,那傳統 IO 爲何要阻塞呢?api

  • epoll 是否須要阻塞呢?數組

  • Java 的 NIO 和 AIO 底層都是 epoll 實現的,這又怎麼理解同步和異步的區別?緩存

三、都是 IO 多路複用。網絡

  • 既生瑜何生亮,爲何會有 select,poll 和 epoll 呢?數據結構

  • 爲何 epoll 比 select 性能高?

PS:

本文共包含三大部分:初識 epoll、epoll 背後的原理 、Diss 環節

本文的重點是介紹原理,建議讀者的關注點儘可能放在:「爲何」。

Linux 下進程和線程的區別其實並不大,尤爲是在討論原理和性能問題時,所以本文中「進程」和「線程」兩個詞是混用的。

3 初識 epoll

epoll 是 Linux 內核的可擴展 I/O 事件通知機制,其最大的特色就是性能優異。下圖是 libevent(一個知名的異步事件處理軟件庫)對 select,poll,epoll ,kqueue 這幾個 I/O 多路複用技術作的性能測試。

不少文章在描述 epoll 性能時都引用了這個基準測試,但少有文章可以清晰的解釋這個測試結果。

這是一個限制了100個活躍鏈接的基準測試,每一個鏈接發生1000次讀寫操做爲止。縱軸是請求的響應時間,橫軸是持有的 socket 句柄數量。隨着句柄數量的增長,epoll 和 kqueue 響應時間幾乎無變化,而 poll 和 select 的響應時間卻增加了很是多。

能夠看出來,epoll 性能是很高的,而且隨着監聽的文件描述符的增長,epoll 的優點更加明顯。

不過,這裏限制的100個鏈接很重要。epoll 在應對大量網絡鏈接時,只有活躍鏈接不多的狀況下才能表現的性能優異。換句話說,epoll 在處理大量非活躍的鏈接時性能纔會表現的優異。若是15000個 socket 都是活躍的,epoll 和 select 其實差不了太多。

爲何 epoll 的高性能有這樣的侷限性?

問題好像愈來愈多了,看來咱們須要更深刻的研究了。

4 epoll背後的原理

4.1 阻塞

4.1.1 爲何阻塞咱們以網卡接收數據舉例,回顧一下以前我分享過的網卡接收數據的過程。

圖片

爲了方便理解,我儘可能簡化技術細節,能夠把接收數據的過程分爲4步:

  1. NIC(網卡) 接收到數據,經過 DMA 方式寫入內存(Ring Buffer 和 sk_buff)。

  2. NIC 發出中斷請求(IRQ),告訴內核有新的數據過來了。

  3. Linux 內核響應中斷,系統切換爲內核態,處理 Interrupt Handler,從RingBuffer 拿出一個 Packet, 並處理協議棧,填充 Socket 並交給用戶進程。

  4. 系統切換爲用戶態,用戶進程處理數據內容。

網卡什麼時候接收到數據是依賴發送方和傳輸路徑的,這個延遲一般都很高,是毫秒(ms)級別的。而應用程序處理數據是納秒(ns)級別的。也就是說整個過程當中,內核態等待數據,處理協議棧是個相對很慢的過程。這麼長的時間裏,用戶態的進程是無事可作的,所以用到了「阻塞(掛起)」。

4.1.2 阻塞不佔用 cpu

阻塞是進程調度的關鍵一環,指的是進程在等待某事件發生以前的等待狀態。請看下錶,在 Linux 中,進程狀態大體有7種(在 include/linux/sched.h 中有更多狀態):

圖片

從說明中其實就能夠發現,「可運行狀態」會佔用 CPU 資源,另外建立和銷燬進程也須要佔用 CPU 資源(內核)。重點是,當進程被"阻塞/掛起"時,是不會佔用 CPU 資源的。

換個角度來說。爲了支持多任務,Linux 實現了進程調度的功能(CPU 時間片的調度)。而這個時間片的切換,只會在「可運行狀態」的進程間進行。所以「阻塞/掛起」的進程是不佔用 CPU 資源的。

另外講個知識點,爲了方便時間片的調度,全部「可運行狀態」狀態的進程,會組成一個隊列,就叫「工做隊列」

4.1.3 阻塞的恢復

內核固然能夠很容易的修改一個進程的狀態,問題是網絡 IO 中,內核該修改那個進程的狀態。

圖片

socket 結構體,包含了兩個重要數據:進程 ID 和端口號。進程 ID 存放的就是執行 connect,send,read 函數,被掛起的進程。在 socket 建立之初,端口號就被肯定了下來,操做系統會維護一個端口號到 socket 的數據結構。

當網卡接收到數據時,數據中必定會帶着端口號,內核就能夠找到對應的 socket,並從中取得「掛起」進程的 ID。將進程的狀態修改成「可運行狀態」(加入到工做隊列)。此時內核代碼執行完畢,將控制權交還給用戶態。經過正常的「CPU 時間片的調度」,用戶進程得以處理數據。

4.1.4 進程模型

上面介紹的整個過程,基本就是 BIO(阻塞 IO)的基本原理了。用戶進程都是獨立的處理本身的業務,這實際上是一種符合進程模型的處理方式。

4.2 上下文切換的優化

上面介紹的過程當中,有兩個地方會形成頻繁的上下文切換,效率可能會很低。

  1. 若是頻繁的收到數據包,NIC 可能頻繁發出中斷請求(IRQ)。CPU 也許在用戶態,也許在內核態,也許還在處理上一條數據的協議棧。但不管如何,CPU 都要儘快的響應中斷。這麼作實際上很是低效,形成了大量的上下文切換,也可能致使用戶進程長時間沒法得到數據。(即便是多核,每次協議棧都沒有處理完,天然沒法交給用戶進程)

  2. 每一個 Packet 對應一個 socket,每一個 socket 對應一個用戶態的進程。這些用戶態進程轉爲「可運行狀態」,必然要引發進程間的上下文切換。

4.2.1 網卡驅動的 NAPI 機制

在 NIC 上,解決頻繁 IRQ 的技術叫作 New API(NAPI) 。原理其實特別簡單,把 Interrupt Handler 分爲兩部分。

  1. 函數名爲 napi_schedule,專門快速響應 IRQ,只記錄必要信息,並在合適的時機發出軟中斷 softirq。

  2. 函數名爲 netrxaction,在另外一個進程中執行,專門響應 napi_schedule 發出的軟中斷,批量的處理 RingBuffer 中的數據。

因此使用了 NAPI 的驅動,接收數據過程能夠簡化描述爲:

  1. NIC 接收到數據,經過 DMA 方式寫入內存(Ring Buffer 和 sk_buff)。

  2. NIC 發出中斷請求(IRQ),告訴內核有新的數據過來了。

  3. driver 的 napi_schedule 函數響應 IRQ,並在合適的時機發出軟中斷(NET_RX_SOFTIRQ)

  4. driver 的 net_rx_action 函數響應軟中斷,從 Ring Buffer 中批量拉取收到的數據。並處理協議棧,填充 Socket 並交給用戶進程。

  5. 系統切換爲用戶態,多個用戶進程切換爲「可運行狀態」,按 CPU 時間片調度,處理數據內容。

一句話歸納就是:等着收到一批數據,再一次批量的處理數據。

4.2.2 單線程的 IO 多路複用

內核優化「進程間上下文切換」的技術叫的「IO 多路複用」,思路和 NAPI 是很接近的。

每一個 socket 再也不阻塞讀寫它的進程,而是用一個專門的線程,批量的處理用戶態數據,這樣就減小了線程間的上下文切換。

圖片

做爲 IO 多路複用的一個實現,select 的原理也很簡單。全部的 socket 統一保存執行 select 函數的(監視進程)進程 ID。任何一個 socket 接收了數據,都會喚醒「監視進程」。內核只要告訴「監視進程」,那些 socket 已經就緒,監視進程就能夠批量處理了。

4.3 IO 多路複用的進化

4.3.1 對比 epoll 與 select

select,poll 和 epoll 都是「IO 多路複用」,那爲何還會有性能差距呢?篇幅限制,這裏咱們只簡單對比 select 和 epoll 的基本原理差別。

對於內核,同時處理的 socket 可能有不少,監視進程也可能有多個。因此監視進程每次「批量處理數據」,都須要告訴內核它「關心的 socket」。內核在喚醒監視進程時,就能夠把「關心的 socket」中,就緒的 socket 傳給監視進程。

換句話說,在執行系統調用 select 或 epoll_create 時,入參是「關心的 socket」,出參是「就緒的 socket」。

而 select 與 epoll 的區別在於:

  • select (一次O(n)查找)

  1. 每次傳給內核一個用戶空間分配的 fd_set 用於表示「關心的 socket」。其結構(至關於 bitset)限制了只能保存1024個 socket。

  2. 每次 socket 狀態變化,內核利用 fd_set 查詢O(1),就能知道監視進程是否關心這個 socket。

  3. 內核是複用了 fd_set 做爲出參,返還給監視進程(因此每次 select 入參須要重置)。

    然而監視進程必須遍歷一遍 socket 數組O(n),才知道哪些 socket 就緒了。

  • epoll (全是O(1)查找)

  1. 每次傳給內核一個實例句柄。這個句柄是在內核分配的紅黑樹 rbr+雙向鏈表 rdllist。只要句柄不變,內核就能複用上次計算的結果。

  2. 每次 socket 狀態變化,內核就能夠快速從 rbr 查詢O(1),監視進程是否關心這個 socket。同時修改 rdllist,因此 rdllist 其實是「就緒的 socket」的一個緩存。

  3. 內核複製 rdllist 的一部分或者所有(LT 和 ET),到專門的 epoll_event 做爲出參。

    因此監視進程,能夠直接一個個處理數據,無需再遍歷確認。

Select 示例代碼

Epoll 示例代碼

另外,epoll_create 底層實現,究竟是不是紅黑樹,其實也不過重要(徹底能夠換成 hashtable)。重要的是 efd 是個指針,其數據結構徹底能夠對外透明的修改爲任意其餘數據結構。

4.3.2 API 發佈的時間線

另外,咱們再來看看網絡 IO 中,各個 api 的發佈時間線。就能夠獲得兩個有意思的結論。

1983,socket 發佈在 Unix(4.2 BSD) 

1983,select 發佈在 Unix(4.2 BSD) 

1994,Linux的1.0,已經支持socket和select 

1997,poll 發佈在 Linux 2.1.23 

2002,epoll發佈在 Linux 2.5.44

一、socket 和 select 是同時發佈的。這說明了,select 不是用來代替傳統 IO 的。這是兩種不一樣的用法(或模型),適用於不一樣的場景。

二、select、poll 和 epoll,這三個「IO 多路複用 API」是相繼發佈的。這說明了,它們是 IO 多路複用的3個進化版本。由於 API 設計缺陷,沒法在不改變 API 的前提下優化內部邏輯。因此用 poll 替代 select,再用 epoll 替代 poll。

4.4 總結

咱們花了三個章節,闡述 Epoll 背後的原理,如今用三句話總結一下。

  1. 基於數據收發的基本原理,系統利用阻塞提升了 CPU 利用率。

  2. 爲了優化上線文切換,設計了「IO 多路複用」(和 NAPI)。

  3. 爲了優化「內核與監視進程的交互」,設計了三個版本的 API(select,poll,epoll)。

5 Diss 環節

講完「Epoll 背後的原理」,已經能夠回答最初的幾個問題。這已是一個完整的文章,不少人勸我刪掉下面的 diss 環節。

個人觀點是:學習就是個研究+理解的過程。上面是研究,下面再講一下個人我的「理解」,歡迎指正。

5.1 關於 IO 模型的分類

關於阻塞,非阻塞,同步,異步的分類,這麼分天然有其道理。可是在操做系統的角度來看「這樣分類,容易產生誤解,並很差」

圖片

5.1.1 阻塞和非阻塞

Linux 下全部的 IO 模型都是阻塞的,這是收發數據的基本原理致使的。阻塞用戶線程是一種高效的方式。

你固然能夠寫一個程序,socket 設置成非阻塞模式,在不使用監視器的狀況下,依靠死循環完成一次 IO 操做。可是這樣作的效率實在是過低了,徹底沒有實際意義。

換句話說,阻塞不是問題,運行纔是問題,運行纔會消耗 CPU。IO 多路複用不是減小了阻塞,是減小了運行。上下文切換纔是問題,IO 多路複用,經過減小運行的進程,有效的減小了上下文切換。

5.1.2 同步和異步

Linux 下全部的 IO 模型都是同步的。BIO 是同步的,select 同步的,poll 同步的,epoll 仍是同步的。

Java 提供的 AIO,也許能夠稱做「異步」的。可是 JVM 是運行在用戶態的,Linux 沒有提供任何的異步支持。所以 JVM 提供的異步支持,和你本身封裝成「異步」的框架是沒有本質區別的(你徹底可使用 BIO 封裝成異步框架)。

所謂的「同步「和」異步」只是兩種事件分發器(event dispatcher)或者說是兩個設計模式(Reactor 和 Proactor)。都是運行在用戶態的,兩個設計模式能有多少性能差別呢?

  • Reactor 對應 java 的 NIO,也就是 Channel,Buffer 和 Selector 構成的核心的 API。

  • Proactor對應 java 的 AIO,也就是 Async 組件和 Future 或 Callback 構成的核心的 API。

5.1.3 個人分類

我認爲 IO 模型只分兩類:

  1. 更加符合程序員理解和使用的,進程模型;

  2. 更加符合操做系統處理邏輯的,IO 多路複用模型。

對於「IO多路複用」的事件分發,又分爲兩類:Reactor 和 Proactor。

5.2 關於 mmap

epoll 到底用沒用到 mmap?

答案:沒有!

這是個以訛傳訛的謠言。其實很容易證實的,用 epoll 寫個 demo。strace 一下就清楚了。

相關文章
相關標籤/搜索