epoll
/ EE-poll /
假設你要盯著一萬個信箱看有沒有新信。舊辦法(select 與 poll)是每一輪走過每一個信箱逐一檢查,連明顯沒信的也查——數量上萬時就慢了。epoll 是更聰明的方案:你只登記信箱一次,系統便維護一份「剛收到信的那些」的清單,於是每一輪你立刻只收到就緒的那幾個。它是 Linux 針對極大量通訊端做 I/O 多工的高效能機制。
具體來說,你用 epoll_create 建立一個 epoll 實例,然後呼叫 epoll_ctl 來新增、修改或移除你關心的通訊端——這對每個通訊端做一次,不是每輪迴圈做。接著 epoll_wait 阻塞到有東西就緒,並只回傳就緒的通訊端,其成本隨就緒事件數量增長,而非隨所盯住的總數。相對地,select 與 poll 在每次呼叫時重掃整組,每次呼叫花 O(n)。epoll 還提供兩種通知方式:水平觸發(只要資料還在,它就持續告訴你某通訊端可讀,像 select)與邊緣觸發(只在就緒狀態改變時告訴你一次,較快,但要求你把通訊端完全讀乾,否則會漏掉資料)。
為什麼重要:epoll 是 Linux 上最具擴展性伺服器內部的引擎——Nginx、Redis、Node.js 與 Go 網路底下的函式庫——因為它讓用少數幾個執行緒服務十萬條以上的並行連線變得實際可行。誠實的提醒:epoll 是 Linux 專屬的(BSD 與 macOS 用 kqueue、Windows 用 IOCP,這正是可攜程式碼使用 libuv 之類包裝的原因),而邊緣觸發模式是惡名昭彰的臭蟲來源——若你沒有一直讀到 recv 會阻塞為止,那些沒讀的位元組就靜靜留在那兒,epoll_wait 也不會再為它們通知你。
伺服器用 epoll_ctl 各登記 50000 個用戶端通訊端一次。每一輪迴圈,epoll_wait 也許回傳實際有資料的 200 個——伺服器只處理那 200 個,幾乎零成本地無視 49800 個閒置的。select 則會每次重新檢查全部 50000 個。
成本隨就緒事件增長,而非隨總連線數。
邊緣觸發的 epoll 每次就緒狀態改變只通知你一次,所以你必須一直讀到 recv 回傳「會阻塞」為止。太早停手,剩下的位元組就被默默擱置——一個經典、難找的臭蟲。