在網際網路初期,伺服器通常採用同步阻塞 I/O(Blocking I/O) 結合「一個連線對應一個執行緒(Thread-per-Connection)」的模型(如早期 Apache HTTP Server)。

當連線數達到數千時,高達數千個作業系統執行緒帶來的執行緒堆疊記憶體消耗與劇烈的 CPU 核心上下文切換(Context Switch) 會直接讓伺服器瀕臨崩潰——這就是著名的 C10K 難題。

為了解決以單一或少量執行緒同時高效監聽數萬乃至數百萬個 Socket 連線的難題,Linux 核心演進出了 I/O 多路復用(I/O Multiplexing) 技術:從 select、poll 到顛覆性的 epoll。

本文將完整剖析這三代技術的底層資料結構、核心態與使用者態複製開銷、以及 Nginx 和 Netty 依賴的 Reactor 事件驅動模式。


1. 三代 I/O 多路復用架構全景對決

評估維度select (1983)poll (1997)epoll (2002)
底層核心資料結構固定大小位元遮罩 Array動態陣列 / 鏈表紅黑樹 + 就緒雙向鏈表
最大監聽連線數限制預設 1024 (FD_SETSIZE)無上限 (受記憶體限制)無上限 (受系統 FD 限制)
FD 從使用者態複製到核心每次呼叫全量重複拷貝每次呼叫全量重複拷貝僅 epoll_ctl 註冊一次
核心就緒檢索複雜度O(N) 全量線性輪詢O(N) 全量線性輪詢O(1) 回調通知立即返回
事件觸發模式僅支援水平觸發 (LT)僅支援水平觸發 (LT)支援 LT 與邊緣觸發 (ET)
適用高併發規模連線數極少 (< 100)連線數較少 (< 1000)百萬級長連線 (C1000K)

2. 第一代 select 與第二代 poll 的瓶頸

2.1 select 的兩大致命缺陷

select 系統呼叫使用 fd_set 位元向量傳遞欲監聽的檔案描述符:

  1. 1024 限制:核心標頭檔宏定義 FD_SETSIZE 固定為 1024,無法直接突破。
  2. 全量拷貝與 O(N) 線性輪詢:
    • 應用程式每次呼叫 select,都必須將包含所有監聽 FD 的陣列從使用者態完整複製到核心態。
    • 核心不知道「究竟哪一個 Socket 收到資料」,只能以 O(N) 遍歷所有 1024 個 FD。
    • 返回後,使用者端應用程式又必須再次以 O(N) 遍歷檢查 FD_ISSET,造成嚴重的 CPU 空轉。

2.2 poll 的微幅改進

poll 採用 pollfd 結構體陣列取代位元遮罩,突破了 1024 的數量限制。然而,其每次呼叫的全量核心態拷貝與核心態/使用者態雙重 O(N) 線性輪詢問題依然完全未獲解決。當監聽 10 萬個連線且只有 10 個有活動時,系統 99.99% 的時間都在做無效掃描。


3. 革命性突破:epoll 核心架構解析

Linux 2.6 核心引入了 epoll,徹底將「註冊監聽」與「等待事件就緒」在核心層面解耦。

3.1 epoll 的核心三部曲 API

  1. int epoll_create(int size):在核心中建立一個 eventpoll 物件。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):向核心註冊、修改或刪除需要監聽的 Socket。
  3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):等待就緒事件,僅返回真正有 I/O 事件發生的 Socket 列表!

3.2 核心內部雙核心資料結構

Linux epoll 核心 eventpoll(紅黑樹 + 就緒鏈表)架構圖展示 eventpoll 包含儲存所有監聽 Socket 的紅黑樹,網卡中斷回調時將活躍節點掛入就緒雙向鏈表,由 epoll_wait 以 O(1) 立即返回。eventpoll (Linux 核心物件)【紅黑樹 (Red-Black Tree)】• 儲存所有註冊監聽的 Socket FD• epoll_ctl 高效 O(log N) 增刪改查• 核心內部長期保留,避免重複拷貝中斷回調【就緒雙向鏈表 (Ready List)】• 僅儲存當前真正觸發 I/O 事件的 Socket• epoll_wait 以 O(1) 立即返回就緒陣列• 徹底消滅 O(N) 百萬連線無效線性輪詢!
  • 零無效輪詢:epoll_wait 永遠只返回就緒列表中的項目,時間複雜度恆定為 O(就緒連線數),完全與總連線數 N 無關!
  • 避免重複拷貝:透過 epoll_ctl 註冊的 Socket 長期保留在核心紅黑樹中,呼叫 epoll_wait 時無需重複傳遞監聽清單。

4. 水平觸發(LT)vs. 邊緣觸發(ET)

epoll 支援兩種事件通知模式:

4.1 水平觸發(Level-Triggered, LT - 預設模式)

  • 行為:只要 Socket 的接收緩衝區(Receive Buffer)中還有未讀取的資料,每次呼叫 epoll_wait 都會持續通知該事件。
  • 優缺點:程式編寫簡單安全,不易遺漏資料;但可能因重複通知帶來微量額外開銷。

4.2 邊緣觸發(Edge-Triggered, ET - 高效能極限)

  • 行為:僅在 Socket 狀態由「未就緒」變為「就緒」的瞬間(狀態跳變) 通知一次!若應用程式未將緩衝區資料一次性讀完,核心不會再發送第二次通知。
  • 編程要求:
    1. Socket 必須設定為非阻塞模式(O_NONBLOCK)。
    2. 應用程式必須使用 while 迴圈持續呼叫 read(),直到回傳 EAGAIN 或 EWOULDBLOCK 為止,否則會造成資料永久丟失!
  • 代表實踐:Nginx 與 Netty 預設採用 ET 模式以追求極限吞吐量。

5. 現代網路伺服器架構:Reactor 模式

基於 epoll,現代高效能架構普遍採用 Reactor 事件驅動模式:

主從 Reactor 多線程事件驅動模型架構圖展示 Client 請求進入 Main Reactor (epoll) 經 Acceptor 分發至 Sub-Reactors (epoll_wait),並交由 Worker Thread Pool 執行業務邏輯。Client RequestsMain Reactor (epoll 監聽連線)Acceptor (分發已建立連線)【 Sub-Reactor 1 ~ N 執行緒池 (各自執行 epoll_wait 負責 Socket read / write) 】非阻塞快速搬移 I/O 資料,與主連線建立徹底解耦Worker Thread Pool (計算密集型業務邏輯處理)非阻塞 I/O 與耗時業務計算分離,榨乾 CPU 極限效能
  • 極致利用 CPU:I/O 執行緒專注於非阻塞資料搬移,計算密集型業務由工作執行緒池處理,徹底發揮多核心硬體的極限算力。