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,并注意错误处理、连接关闭和输出缓冲区管理。