为什么不用 select/poll
// select 的历史包袱
fd_set readfds; // FD_SETSIZE=1024 硬限制
FD_ZERO(&readfds); // 每次调用都要重新设置
FD_SET(sock, &readfds); // 设置关心哪个 fd
select(maxfd+1, &readfds, NULL, NULL, NULL);
// 返回后遍历所有 fd 检查 → O(n) 扫描
// epoll → O(1) 只返回就绪的 fd
epoll 三步
#include <sys/epoll.h>
// ① 创建
int epfd = epoll_create1(0);
// ② 注册
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 读事件 + 边缘触发
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
// ③ 等待
const int MAX_EVENTS = 64;
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1); // -1 = 无限等待
for (int i = 0; i < n; ++i) {
if (events[i].events & EPOLLIN)
handle_read(events[i].data.fd);
}
边缘触发 (ET) vs 水平触发 (LT)
| LT (默认) | ET |
|---|
| 通知时机 | 只要缓冲区有数据 | 仅当新数据到达 |
| 处理要求 | 可以分次读 | 必须读到 EAGAIN |
| 适用 | 简单、可靠 | 高性能 |
// ET 必须循环读到 EAGAIN
void handle_read(int fd) {
char buf[4096];
while (true) {
ssize_t n = read(fd, buf, sizeof(buf));
if (n > 0) process(buf, n);
else if (n == 0) { close(fd); break; } // 对端关闭
else if (errno == EAGAIN || errno == EWOULDBLOCK) break;
else { perror("read"); break; }
}
}
epoll_data 的妙用
// 不只存 fd,还能存指针
struct Connection {
int fd;
std::string read_buf;
// ...
};
Connection *conn = new Connection{client_fd};
ev.events = EPOLLIN | EPOLLET;
ev.data.ptr = conn; // 存指针!
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
// 事件回来时直接拿到上下文
for (int i = 0; i < n; ++i) {
auto *conn = static_cast<Connection *>(events[i].data.ptr);
handle(conn); // 不需要查找 fd → conn 映射
}
对比
| select | poll | epoll |
|---|
| 最大 fd 数 | 1024 | 无限制 | 无限制 |
| 检测方式 | 全部遍历 O(n) | 全部遍历 O(n) | 只返回就绪 O(1) |
| 注册方式 | 每次重新设置 | 每次重新设置 | 注册一次 |
| 内核实现 | 轮询 | 轮询 | 回调 + 就绪队列 |