Limesn
Limesn
发布于 2026-09-16 / 11 阅读
0

epoll 使用详解

1. 概述

在 Linux 网络编程中,早期常使用 select 实现 I/O 多路复用。但 select 存在明显缺点:

  • 采用轮询方式处理,监听的 fd 越多,效率越低;
  • 受 fd_set 限制,默认最多监听 1024 个 fd;
  • 每次调用都需要重新传入 fd 集合,用户态与内核态之间复制开销较大。

epoll 是 Linux 内核提供的一种更高效的 I/O 多路复用机制。它通过内核事件表管理 fd,只返回就绪事件,因此效率不会随监听 fd 总数的增长而线性下降。

epoll 的核心接口只有三个:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

使用完毕后,还需要调用 close() 关闭 epoll 句柄。


2. 核心 API

2.1 epoll_create

int epoll_create(int size);

创建一个 epoll 句柄,返回一个文件描述符。

  • size 用于告诉内核这个 epoll 对象大致会处理多少事件。
  • 在现代 Linux 内核中,size 参数已经没有实际意义,但必须大于 0。
  • epoll 句柄本身也会占用一个 fd,使用完后必须 close(),否则可能导致 fd 耗尽。
  • 推荐使用 epoll_create1(EPOLL_CLOEXEC),可以设置 close-on-exec 标志。

示例:

int epfd = epoll_create1(0);
if (epfd < 0) {
    perror("epoll_create1");
    exit(1);
}

2.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 用于向 epoll 对象中添加、修改或删除感兴趣的事件。

参数说明:

  • epfd:epoll_create() 返回的句柄。
  • op:操作类型,有三个宏:
    • EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
    • EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
    • EPOLL_CTL_DEL:从 epfd 中删除一个 fd。
  • fd:需要监听的 fd。
  • event:告诉内核需要监听什么事件。

返回值:成功返回 0,失败返回 -1,并设置 errno。

注意:epoll_wait 返回的事件,必然是通过 epoll_ctl 添加到 epoll 中的事件。


2.3 epoll_event 结构

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events;   /* Epoll events */
    epoll_data_t data;   /* User data variable */
};

常用事件宏:

  • EPOLLIN:对应的文件描述符可以读,包括对端 SOCKET 正常关闭;
  • EPOLLOUT:对应的文件描述符可以写;
  • EPOLLPRI:有紧急数据可读;
  • EPOLLERR:发生错误;
  • EPOLLHUP:被挂断;
  • EPOLLET:设置为边缘触发模式;
  • EPOLLONESHOT:只监听一次事件,之后需要重新注册;
  • EPOLLRDHUP:对端关闭连接或半关闭,Linux 2.6.17+ 支持。

2.4 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待事件产生,类似于 select()。

参数说明:

  • epfd:epoll 描述符;
  • events:用户分配好的 epoll_event 数组,内核将就绪事件复制到该数组中;
  • maxevents:本次最多返回的事件数目,必须大于 0;
  • timeout:超时时间,单位为毫秒:
    • 0:立即返回;
    • -1:永久阻塞,直到有事件发生;
    • >0:等待指定毫秒数。

返回值:

  • >0:返回需要处理的事件数目;
  • 0:已超时;
  • -1:出错,需要检查 errno。

注意:maxevents 与 epoll_create() 的 size 参数没有直接关系,原文中“不能大于 size”的说法并不准确。


2.5 close

使用完 epoll 后,必须调用 close() 关闭 epoll 句柄。

close(epfd);

另外,关闭被监听的 fd 时,该 fd 会自动从 epoll 中移除。但为了逻辑清晰,建议显式调用 EPOLL_CTL_DEL。


3. LT 与 ET 工作模式

epoll 有两种工作模式:

3.1 LT 模式(水平触发,Level Triggered)

默认模式。

特点:

  • 只要缓冲区中还有数据,epoll_wait 就会持续通知;
  • 可以处理阻塞和非阻塞套接字;
  • 编程简单,不容易丢事件。

3.2 ET 模式(边缘触发,Edge Triggered)

通过 EPOLLET 设置。

特点:

  • 仅当状态发生变化时才通知一次;
  • 效率更高;
  • 必须配合非阻塞套接字;
  • 读写时必须一直循环,直到 read/write 返回 EAGAIN 或 EWOULDBLOCK。

ET 模式常见问题:如果一次没有把缓冲区数据读完,之后没有新事件到来时,epoll_wait 不会再通知,导致数据得不到处理。

因此,ET 模式下必须:

while ((n = read(fd, buf, sizeof(buf))) > 0) {
    // 处理数据
}
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
    // 处理错误
}

Nginx 默认使用 ET 模式。


4. epoll 使用框架

几乎所有的 epoll 程序都遵循如下框架:

for (;;) {
    nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);

    for (i = 0; i < nfds; ++i) {
        if (events[i].data.fd == listenfd) {
            // 有新连接
            // ET 模式下需要循环 accept 直到 EAGAIN
        } else if (events[i].events & EPOLLIN) {
            // 可读
            // ET 模式下需要循环 read 直到 EAGAIN
        } else if (events[i].events & EPOLLOUT) {
            // 可写
        } else {
            // 其他处理
        }
    }
}

如果网络主循环单独在一个线程中,timeout 可以使用 -1,以提高效率。如果与主逻辑在同一线程,可以使用 0,避免阻塞主循环。


5. 完整示例:基于 ET 的简单 echo 服务器

以下示例仅用于教学,简化了写缓冲和部分错误处理。生产环境需要更完善的缓冲区管理和连接状态管理。

#include <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <cstring>
#include <cstdlib>

#define MAX_EVENTS 64
#define LISTENQ 128
#define BUFSIZE 1024

void setnonblocking(int fd) {
    int opts = fcntl(fd, F_GETFL);
    if (opts < 0) {
        perror("fcntl F_GETFL");
        exit(1);
    }
    if (fcntl(fd, F_SETFL, opts | O_NONBLOCK) < 0) {
        perror("fcntl F_SETFL");
        exit(1);
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " port\n";
        return 1;
    }

    int port = atoi(argv[1]);

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        perror("socket");
        return 1;
    }
    setnonblocking(listenfd);

    int opt = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    sockaddr_in serveraddr{};
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(port);

    if (bind(listenfd, (sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) {
        perror("bind");
        return 1;
    }

    if (listen(listenfd, LISTENQ) < 0) {
        perror("listen");
        return 1;
    }

    int epfd = epoll_create1(0);
    if (epfd < 0) {
        perror("epoll_create1");
        return 1;
    }

    epoll_event ev{};
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = listenfd;

    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0) {
        perror("epoll_ctl add listenfd");
        return 1;
    }

    epoll_event events[MAX_EVENTS];
    char buf[BUFSIZE];

    for (;;) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nfds < 0) {
            if (errno == EINTR) continue;
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < nfds; ++i) {
            int fd = events[i].data.fd;

            if (fd == listenfd) {
                // ET 模式:循环 accept 直到 EAGAIN
                for (;;) {
                    sockaddr_in clientaddr{};
                    socklen_t clilen = sizeof(clientaddr);

                    int connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);
                    if (connfd < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        }
                        perror("accept");
                        break;
                    }

                    setnonblocking(connfd);
                    std::cout << "accept from " << inet_ntoa(clientaddr.sin_addr) << std::endl;

                    epoll_event conn_ev{};
                    conn_ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
                    conn_ev.data.fd = connfd;

                    if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &conn_ev) < 0) {
                        perror("epoll_ctl add connfd");
                        close(connfd);
                    }
                }
            } else if (events[i].events & EPOLLIN) {
                // ET 模式:循环 read 直到 EAGAIN
                for (;;) {
                    ssize_t n = read(fd, buf, sizeof(buf));

                    if (n < 0) {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        }
                        if (errno == ECONNRESET) {
                            close(fd);
                            break;
                        }
                        perror("read");
                        close(fd);
                        break;
                    } else if (n == 0) {
                        // 对端关闭
                        close(fd);
                        break;
                    } else {
                        // 简单回显:生产环境需处理非阻塞写不完整问题
                        ssize_t written = 0;
                        while (written < n) {
                            ssize_t w = write(fd, buf + written, n - written);
                            if (w < 0) {
                                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                    // 简化处理:等待下次可写
                                    // 生产环境应缓冲剩余数据并注册 EPOLLOUT
                                    break;
                                }
                                perror("write");
                                close(fd);
                                break;
                            }
                            written += w;
                        }
                    }
                }
            } else if (events[i].events & (EPOLLHUP | EPOLLERR | EPOLLRDHUP)) {
                close(fd);
            }
        }
    }

    close(listenfd);
    close(epfd);
    return 0;
}

6. 总结

  • epoll 是 Linux 下高性能 I/O 多路复用机制,适合高并发网络服务器。
  • 核心 API:epoll_create、epoll_ctl、epoll_wait、close。
  • 默认使用 LT 模式,编程简单;ET 模式效率更高,但必须配合非阻塞 fd,并循环读写直到 EAGAIN。
  • epoll_wait 的 maxevents 与 epoll_create 的 size 没有直接关系。
  • 使用完 epoll 句柄后必须 close。
  • 推荐使用 epoll_create1,并注意错误处理、连接关闭和输出缓冲区管理。