一個 Thread 要同時看顧上萬條連線,難處不在「怎麼讀資料」,而在「怎麼知道哪一條有資料」。select 與 poll 的答案是每次都問過所有人;epoll 的答案是讓有事的那條自己舉手——這一個反轉,就是 O(N) 變成 O(1) 的全部原因。
傳統的 select() 和 poll() 雖然讓單一 Thread 能監聽多個 Socket,但連線數量達到數萬時,O(N) 線性掃描讓 CPU 使用率暴增——即使 99% 的 Socket 都沒資料,核心仍然逐一檢查每一個。
epoll_create()O(1)epoll_ctl()O(log N)epoll_wait()O(就緒數量)create → ctl(ADD) 逐一註冊 Socket → wait 阻塞等待 → 處理就緒清單 → 回到 wait。Epoll 支援兩種事件觸發模式,這是面試與實作中最常踩的細節:
errno == EAGAIN。epoll_ctl ADD 進來的 Socket fd,增刪查都是 O(log N)。加入的同時,核心在該 Socket 的等待佇列掛上一個 callback。琥珀色格子代表 CPU 正在檢查這個 Socket。Select 必須掃過全部 100 個;Epoll 由核心 Callback 推送,直接跳到有事件的那 5 個。
# 系統就緒,選擇機制開始模擬。
// 連線數 ×10 時兩者差距將更顯著。
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大連線數 | 1024(FD_SETSIZE 硬限制) | 無限制 | 無限制 |
| 等待複雜度 | O(N) | O(N) | O(1) |
| fd 集合傳遞 | 每次 syscall 都複製到核心 | 每次 syscall 都複製到核心 | ctl ADD 一次,不重複複製 |
| 就緒事件識別 | 核心線性掃描全部 fd | 核心線性掃描全部 fd | Callback 主動推送,只返回就緒 fd |
| 觸發模式 | 只有 LT | 只有 LT | LT + ET 均支援 |
| 跨平台 | Unix / Windows | Unix | Linux 限定(BSD 用 kqueue) |
| 適合場景 | 連線數 < 100 的簡單程式 | 中等連線數 + 跨平台需求 | C10K+ 高並發伺服器 |
兩條線的形狀差異比絕對數字重要:一條是斜率固定的直線,一條幾乎貼著底。
pollfd 陣列取代 fd_set,拿掉 1024 上限——但依然 O(N) 掃描、依然每次複製。本質沒有改變,只是把天花板拆了。
use epoll;。EventEmitter 的底層就是 epoll 通知。Selector 在 Linux 下自動使用 epoll;JDK 11+ 另有 EpollEventLoop 直接透過 JNI 呼叫,繞過 Java 層開銷。top 裡 sy(system)CPU 佔比過高strace -p <PID> -e epoll_wait —— 看得到 epoll_wait 被反覆呼叫就對了。
cat /proc/sys/fs/file-max 系統最大 fd 數;ulimit -n 目前用戶限制(預設 1024,高並發要調到 65535+)。
epoll_wait 的複雜度是 O(就緒數量),與總連線數無關。