epoll(Linux)與 kqueue(BSD)
/ epoll -> EE-poll; kqueue -> KAY-queue /
想像一間夜店,賓客名單上有五萬個名字。用 poll() 時,門口的保鏢每次查驗都把整份名單重讀一遍。更聰明的做法是把每位賓客向場館登記「一次」,之後只要被遞上一張短紙條,指名剛剛到的是誰。Linux 的 epoll 與 BSD、macOS 的 kqueue 就是這個更聰明的做法:一個位於核心側、持久的關注集合,你把描述符登記進去一次,此後每次等待只回傳真正變就緒的那些描述符。
以 epoll 具體來說:你用 epoll_create1() 建立一個 epoll 實例,它回給你一個特殊描述符。你用 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) 把每個你在意的通訊端加進去——這個登記會被核心記在多次呼叫之間。接著你呼叫 epoll_wait(),它會阻塞,返回時把一個陣列填上「就緒的事件」,並用回傳值告訴你有幾個。核心在 I/O 完成時於內部維護一張就緒清單,所以一次「在五萬個之中找到三個就緒通訊端」的等待,代價是 O(3),而不是 O(50000)。kqueue 是同一個概念,用單一統合的 kevent() 呼叫同時登記關注(changelist)並收取就緒事件(eventlist),而且它不只能看通訊端,還能看計時器、信號、檔案變更與行程事件。
為何重要:epoll 與 kqueue 才是真正解決 C10k 問題的東西,並支撐了幾乎每一個現代高效能伺服器與非同步執行時(nginx、Redis、Node.js、libuv、Netty、Tokio 等等)。它們是就緒式、而非完成式:它們告訴你一個通訊端就緒了,read() 仍由你自己做。它們彼此也不可攜——epoll 只在 Linux、kqueue 屬於 BSD/macOS 家族——這正是跨平台函式庫把兩者包在同一介面之後的原因。
int ep = epoll_create1(0); struct epoll_event ev = { .events = EPOLLIN, .data.fd = sock }; epoll_ctl(ep, EPOLL_CTL_ADD, sock, &ev); struct epoll_event out[64]; int n = epoll_wait(ep, out, 64, -1); /* n 個就緒事件 */
用 epoll_ctl() 把通訊端登記一次;之後每次 epoll_wait() 只回傳就緒的描述符。
epoll 的擴展是 O(就緒)、而非 O(被監看),但它仍是就緒式:等待之後你必須自己呼叫 read()/accept(),並正確處理 EAGAIN。常見陷阱是忘了用 EPOLL_CTL_DEL 移除已關閉的描述符,或誤以為 epoll 跨平台地像 select 一樣運作——它在 Linux 之外根本不存在。