epoll 是 Linux 2.5.44 引入的高效 I/O 多路複用機制,解決 select/poll 在大連接數下的 O(n) 掃描、重複拷貝、重複掛載隊列三大痛點。本文系統講解 epoll 創建動機、核心優勢、LT/ET 兩種觸發模式、三大 API 詳解、完整服務器範例代碼與關鍵優化要點。
| 優勢 | 技術實現 |
|---|---|
| 無 FD 數量上限 | 支援至系統 /proc/sys/fs/file-max(1GB 記憶體約 10 萬) |
| 效能不隨 FD 數線性下降 | 僅處理「就緒」fd,核心以回調函數驅動,非輪詢 |
| mmap 零拷貝 | 核心與用戶空間共享就緒事件環狀緩衝區 |
| 內核可調優 | TCP 記憶體池、listen backlog、NAPI 驅動等可運行時調優 |
| 特性 | LT (Level Triggered) | ET (Edge Triggered) |
|---|---|---|
| 預設模式 | 是 | 否(需指定 EPOLLET) |
| 支援阻塞/非阻塞 | 皆支援 | 僅支援非阻塞 |
| 就緒通知語意 | 「狀態就緒」持續通知,直到處理完 | 「狀態變化」瞬間通知一次,不再重複 |
| 程式設計難度 | 低,不易漏讀 | 高,必須循環讀寫至 EAGAIN |
| 典型適用 | 一般網路服務 | 極高併發、零拷貝、狀態機驅動 |
ET 模式核心循環:while ((n = read(fd, buf, sizeof(buf))) > 0) process(buf); if (n == -1 && errno != EAGAIN) error;
int epoll_create(int size)size 提示監聽規模(內核 2.6.8+ 忽略,僅驗證 >0)。int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)| op | 宏 | 用途 |
|---|---|---|
| ADD | EPOLL_CTL_ADD | 註冊新 fd |
| MOD | EPOLL_CTL_MOD | 修改已註冊 fd 事件 |
| DEL | EPOLL_CTL_DEL | 移除 fd(event 可為 NULL) |
struct epoll_event 關鍵欄位:
uint32_t events; // 事件掩碼:EPOLLIN/EPOLLOUT/EPOLLERR/EPOLLET/EPOLLONESHOT...
epoll_data_t data; // 用戶自定義資料:ptr/fd/u32/u64
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)timeout:-1 永久、0 立即返回、>0 毫秒。events 陣列由核心填充(mmap 共享記憶體,零拷貝)。#include
#include
#include
#include
#define MAXLINE 5
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5000
void setnonblocking(int sock) {
int opts = fcntl(sock, F_GETFL);
opts |= O_NONBLOCK;
fcntl(sock, F_SETFL, opts);
}
int main(int argc, char *argv[]) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd);
struct sockaddr_in servaddr = {0};
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
int epfd = epoll_create(256);
struct epoll_event ev, events[20];
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
for (;;) {
int nfds = epoll_wait(epfd, events, 20, 500);
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listenfd) { // 新連接
int connfd = accept(listenfd, NULL, NULL);
setnonblocking(connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) { // 可讀
int sockfd = events[i].data.fd;
char line[MAXLINE];
int n = read(sockfd, line, MAXLINE);
if (n <= 0) { close(sockfd); events[i].data.fd = -1; continue; }
line[n] = '\0';
std::cout << "read: " << line << std::endl;
ev.data.fd = sockfd;
ev.events = EPOLLOUT | EPOLLET; // 切換到可寫
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
} else if (events[i].events & EPOLLOUT) { // 可寫
int sockfd = events[i].data.fd;
write(sockfd, line, n); // 此處 line/n 需保存上下文
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET; // 切回可讀
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
}
}
return 0;
}
關鍵點註解:
setnonblocking:ET 模式強制非阻塞,否則 read/write 會阻塞整個事件循環。EPOLLET:邊緣觸發,配合非阻塞實現高效零拷貝。epoll_ctl MOD:動態切換關注事件(讀↔寫),避免忙輪詢。sockfd → buffer 映射,處理分包/黏包、錯誤碼、關閉序列。MOD 重新註冊。/proc/sys/fs/file-max、ulimit -n、somaxconn、TCP 記憶體參數。EAGAIN,否則會漏事件導致連接卡死。epoll 以「事件驅動 + 就緒鏈表 + mmap 共享」三大創新,徹底解決 select/poll 規模瓶頸。掌握 epoll_create/ctl/wait 三板斧、LT/ET 選型原則、非阻塞邊緣觸發讀寫循環,即可構建百萬級併發網路服務。