在 Linux 網絡編程中,epoll 是取代 select/poll 的高性能 I/O 多路復用機制。本文深入剖析 epoll 三大核心 API、邊緣觸發(ET)與水平觸發(LT)模式的本質區別、代碼實戰模式,以及常見坑點與最佳實踐。
select/poll 每次調用需輪詢所有監聽的文件描述符(FD),FD 數量增大時 CPU 開銷線性增長。select 受 FD_SETSIZE(通常 1024)限制,雖可修改內核重編,但治標不治本。epoll 通過內核事件就緒隊列+紅黑樹管理監聽 FD,實現 O(1) 事件通知、無 FD 數量硬限制(僅受系統 max_user_watches 及文件描述符上限約束)、零拷貝(mmap 共享就緒事件環形緩衝區)。
epfd(佔用一個 FD,/proc//fd/ 可見)。size:建議監聽 FD 總數,**非**最大 FD 值。內核 2.6.8+ 忽略此參數,動態擴展。close(epfd) 釋放,否則 FD 洩漏導致進程無法創建新 FD。註冊/修改/刪除監聽事件,與 select 在調用時傳遞 FD 集合截然不同。
op:EPOLL_CTL_ADD(註冊)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(刪除)。event.events:位掩碼,常用值:| 宏 | 含義 |
|---|---|
EPOLLIN | 可讀(含對端正常關閉 FIN) |
EPOLLOUT | 可寫 |
EPOLLRDHUP | 對端關閉連接/半關閉(Linux 2.6.17+,建議配合 EPOLLIN 使用) |
EPOLLPRI | 帶外數據/緊急數據可讀 |
EPOLLERR | FD 發生錯誤(無需註冊,自動觸發) |
EPOLLHUP | FD 掛起(無需註冊,自動觸發) |
EPOLLET | 啟用邊緣觸發(ET)模式 |
EPOLLONESHOT | 單次觸發,處理完需 EPOLL_CTL_MOD 重新啟用 |
event.data:用戶自定義數據(通常存 FD 或指向連接上下文的指針)。timeout:-1 永久阻塞,0 非阻塞立即返回,>0 毫秒超時。maxevents:events 數組容量,**不可大於** epoll_create 的 size(舊內核限制,新內核無此限)。| 維度 | Level Triggered (LT,默認) | Edge Triggered (ET,需 EPOLLET) |
|---|---|---|
| 觸發條件 | FD 處於就緒狀態(如讀緩衝區有數據) | FD 從非就緒變為就緒(狀態變化瞬間) |
| 重複通知 | 是,未處理完會反覆通知 | 否,只通知一次,直到再次狀態變化 |
| 支持阻塞 FD | 是 | 否,必須非阻塞 FD(否則單 FD 阻塞導致整個事件循環饑餓) |
| 編程複雜度 | 低,不易遺漏事件 | 高,需循環讀寫直到 EAGAIN/EWOULDBLOCK |
| 典型場景 | 簡單服務、快速原型 | 高併發、長連接、極致性能(Nginx、Redis、Memcached 均用 ET) |
// 讀循環
while (true) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) {
process(buf, n);
if (n < sizeof(buf)) break; // 緩衝區已讀空,事件處理完成
} else if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
break; // 非阻塞無數據,事件處理完成
} else if (n == 0) {
close_conn(); // 對端關閉
break;
} else {
handle_error(); // 其他錯誤
break;
}
}
// 寫循環(發送緩衝區未滿時觸發 EPOLLOUT)
while (remaining > 0) {
ssize_t n = write(fd, ptr, remaining);
if (n > 0) { ptr += n; remaining -= n; }
else if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 發送緩衝區滿,重新註冊 EPOLLOUT 等待下次可寫
mod_event(fd, EPOLLIN | EPOLLOUT);
break;
} else { handle_error(); break; }
}
if (remaining == 0) mod_event(fd, EPOLLIN); // 全部發完,取消 EPOLLOUT
核心原則:ET 模式下,**必須循環讀寫直到 EAGAIN**,否則數據滯留內核緩衝區不再觸發事件,導致「假死」。
啟用後,事件觸發一次即自動從 epoll 移除,需調用 epoll_ctl(..., EPOLL_CTL_MOD, ...) 重新註冊。典型用於線程池模型:主線程 epoll_wait 分發任務,工作線程處理完後 MOD 重新啟用監聽,避免同一連接在多線程並發處理導致數據亂序。
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK),ET 模式強制要求。EPOLLRDHUP:可靠檢測對端關閉,避免誤判為錯誤。epfd 時,內核 4.5+ 支持 EPOLLEXCLUSIVE 標誌(配合 EPOLLIN),實現內核級負載均衡。EPOLLERR/EPOLLHUP:無需註冊自動觸發,統一在事件循環中關閉連接、清理資源。timerfd + epoll 統一事件源。ulimit -n、/etc/security/limits.conf、fs.file-max 三層配置,確保支撐高併發。epoll 是 Linux 高性能網絡編程的基石。選擇 LT 降低認知負荷,選擇 ET 榨乾性能;無論哪種模式,**非阻塞 FD + 循環讀寫到 EAGAIN + 正確錯誤處理** 是編寫健壯事件循環的鐵律。深入理解內核就緒隊列機制,方能駕馭百萬級併發架構。
Linux 下 Apache 服務管理命令大全:啟動、停止、重啟、平滑重載
Linux 核心新增驅動程式完整教學:Kconfig、Makefile 與選單配置實戰
Linux 網路程式設計:健壯的讀寫函數實作(處理 EINTR、EPIPE 等錯誤)
Linux 服務器間文件同步實戰:Rsync 守護進程模式配置與定時同步策略
Linux 修改檔案/裝置擁有者:chown -R 遞歸變更實例
Linux 新手必知的 10 大基礎觀念:從大小寫敏感到隱藏檔管理
MySQL Socket 連線失敗:/var/lib/mysql/mysql.sock 與 /tmp/mysql.sock 路徑衝突完整解決方案