输入关键词开始搜索

Linux epoll — IO 多路复用

为什么不用 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 映射
}

对比

selectpollepoll
最大 fd 数1024无限制无限制
检测方式全部遍历 O(n)全部遍历 O(n)只返回就绪 O(1)
注册方式每次重新设置每次重新设置注册一次
内核实现轮询轮询回调 + 就绪队列