Linux 的 epoll
是一个强大的 I/O 事件通知机制,专为处理大量并发文件描述符(尤其是网络套接字)而设计。它是对早期 select
和 poll
系统调用的高效替代。
设计思想
- 内核事件表: 在内核中维护一个 事件表(一个高效的数据结构,通常是红黑树 + 就绪链表)。用户程序通过
epoll_create
创建一个 epoll 实例(epfd
),该实例对应这个内核事件表。 - 注册/修改/删除: 使用
epoll_ctl
单独地 向这个内核事件表添加 (EPOLL_CTL_ADD
)、修改 (EPOLL_CTL_MOD
) 或删除 (EPOLL_CTL_DEL
) 需要监听的描述符及其关注的事件类型(读、写、异常等)。避免了每次调用传递整个集合。 - 事件等待: 使用
epoll_wait
等待事件发生。内核只会将 就绪的事件 填充到用户提供的缓冲区中。用户程序只需处理返回的就绪事件,避免了扫描整个集合。 返回的就绪事件数量通常远小于注册的总事件数 (O(1)
或O(m)
,m
是就绪事件数)。
epoll_create() & epoll_create1()
int epoll_create(int size);
。int epoll_create1(int flags);
。
创建 epoll 实例的核心对象,返回其文件描述符。
推荐使用 epoll_create1()
。
size
- 原始意图:提示内核期望监听的描述符数量(内核据此分配初始空间)。
- 现代内核行为(Linux 2.6.8+):
- 参数被忽略,内核动态调整数据结构大小。
- 必须 > 0(否则返回
EINVAL
错误)。
- 兼容性建议:传入正整数(如
1
),无需精确计算。
flags
flags
:位掩码,控制 epoll 实例行为(支持按位或组合):
标志 | 值 | 作用 |
---|---|---|
EPOLL_CLOEXEC | 02000000 | 设置 close-on-exec 标志 ( exec() 时自动关闭 epoll 实例) |
其他标志 | - | 当前 Linux 未定义其他标志 |
return
返回值: 成功返回 epfd
(非负整数),失败返回 -1 并设置 errno
。
错误码 | 原因 |
---|---|
EINVAL | size ≤ 0 或 指定了无效的 flags |
EMFILE | 进程打开的文件描述符数量超过限制 |
ENFILE | 系统打开的文件总数超过限制 |
ENOMEM | 内存不足 |
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd == -1) {
switch (errno) {
case EINVAL:
fprintf(stderr, "Invalid flags specified\n");
break;
case EMFILE:
fprintf(stderr, "Process file descriptor limit reached\n");
break;
case ENFILE:
fprintf(stderr, "System-wide file descriptor limit exceeded\n");
break;
case ENOMEM:
fprintf(stderr, "Insufficient kernel memory\n");
break;
default:
perror("Unknown epoll_create1 error");
}
exit(EXIT_FAILURE);
}
epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
。
用于管理 epoll 实例(epfd
)中的文件描述符监听列表,执行添加、修改或删除操作。
epfd
有效的 epoll 文件描述符。
op
指定的操作类型。
操作 | 值 | 说明 |
---|---|---|
EPOLL_CTL_ADD | 1 | 添加新 fd 到监听列表,并关联 event 中指定的事件 |
EPOLL_CTL_MOD | 2 | 修改已注册 fd 的监听事件,并指定的新事件 |
EPOLL_CTL_DEL | 3 | 从监听列表中删除 fd,event 参数可忽略(设为 NULL ) |
fd
操作的目标文件描述符。
要求:
- 必须是有效的文件描述符。
- 支持的类型:socket、pipe、tty、eventfd、timerfd 等。
- 不支持:常规文件(返回
EPERM
错误)。
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_data_t data; // 用户数据
};
events
事件标志详解:
标志 | 值 | 说明 |
---|---|---|
EPOLLIN | 0x001 | 数据可读(包括对端关闭连接) |
EPOLLOUT | 0x004 | 数据可写 |
EPOLLRDHUP | 0x2000 | 对端关闭连接(需显式设置) |
EPOLLPRI | 0x002 | 紧急数据可读(带外数据) |
EPOLLERR | 0x008 | 错误发生(总是监控) |
EPOLLHUP | 0x010 | 挂起/断开连接(总是监控) |
EPOLLET | 0x80000000 | 边缘触发模式(默认水平触发) |
EPOLLONESHOT | 0x40000000 | 单次事件通知(需重新激活) |
EPOLLWAKEUP | 0x20000000 | 阻止系统休眠(需 CAP_BLOCK_SUSPEND) |
EPOLLEXCLUSIVE | 0x10000000 | 避免惊群效应(Linux 4.5+) |
return
返回值: 成功返回 0,失败返回 -1 并设置 errno
。
错误码 | 原因 |
---|---|
EBADF | epfd 或 fd 是无效文件描述符 |
EEXIST | op=ADD 但 fd 已注册 |
EINVAL | 无效参数: - epfd 不是 epoll 实例- op 无效- fd 不支持 epoll- events 包含无效标志 |
ELOOP | fd 是 epoll 实例自身(自监听) |
ENOENT | op=MOD/DEL 但 fd 未注册 |
ENOMEM | 内存不足 |
EPERM | fd 不支持 epoll(如常规文件) |
ENOSPC | 超过 /proc/sys/fs/epoll/max_user_watches 限制 |
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd < 0) {
fprintf(stderr, "create epoll error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
fprintf(stderr, "create socket error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
struct my_data {
uint16_t data;
};
my_data *d = new my_data{1}; // 数据的生命周期要至少和ev一样大
epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 监听该socket是否可读 + ET触发
ev.data.fd = fd;
ev.data.ptr = d;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
delete d;
fprintf(stderr, "epoll ctl error: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
close(epfd);
epoll_wait()
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
。
用于等待注册的文件描述符上发生事件。
epfd
有效的 epoll 文件描述符。
events
用户分配的缓冲区,用于接收就绪事件。
- 内存大小 =
maxevents * sizeof(struct epoll_event)
。
maxevents
指定 events
数组能接收的最大事件数(必须大于 0)。
timeout
等待超时时间(毫秒)。
值 | 行为 |
---|---|
-1 | 无限期阻塞(永久等待) |
0 | 立即返回(非阻塞轮询) |
>0 | 等待指定毫秒数 |
return
返回值 | 含义 |
---|---|
>0 | 返回就绪的文件描述符数量(即 events 中有效元素个数) |
0 | 超时时间内没有事件发生 |
-1 | 出错(检查 errno ) |
错误码 | 原因 |
---|---|
EBADF | epfd 不是有效的文件描述符 |
EFAULT | events 指向的内存不可访问 |
EINTR | 调用被信号中断(可安全重启) |
EINVAL | epfd 不是 epoll 文件描述符,或 maxevents ≤ 0 |
#define MAX_EVENTS 64
int main() {
int epfd = epoll_create1(EPOLL_CLOEXEC);
// ... (添加监听描述符 epoll_ctl)
struct epoll_event events[MAX_EVENTS];
while (1) {
// 等待事件(最多1秒)
int n = epoll_wait(epfd, events, MAX_EVENTS, 1000);
if (n == -1) {
if (errno == EINTR) continue; // 被信号中断,重试
perror("epoll_wait");
break;
}
if (n == 0) {
// 超时处理(可选)
continue;
}
// 处理就绪事件
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
uint32_t revents = events[i].events;
// 错误检查(必须优先处理)
if (revents & (EPOLLERR | EPOLLHUP)) {
handle_error(fd);
continue;
}
// 可读事件
if (revents & EPOLLIN) {
if (is_listen_socket(fd)) {
accept_connections(fd, epfd);
} else {
handle_read(fd);
}
}
// 可写事件
if (revents & EPOLLOUT) {
handle_write(fd);
}
// 对端关闭
if (revents & EPOLLRDHUP) {
handle_peer_close(fd);
}
}
}
}
close
int close(int fd);
。
关闭文件描述符。
LT vs ET
水平触发 (Level-Triggered - LT) (默认):
- 核心原则:只要文件描述符处于就绪状态(读缓冲区非空、写缓冲区有空间),就会持续通知你。
- 行为:
- 当
epoll_wait
报告一个EPOLLIN
事件后,你可以不一次性把所有数据读完。只要下次调用epoll_wait
时该 socket 的接收缓冲区中还有数据未读,epoll_wait
会再次报告它(即再次返回这个fd
的EPOLLIN
事件)。 - 当
epoll_wait
报告一个EPOLLOUT
事件后,你可以不一次性把所有数据写完。只要下次调用epoll_wait
时该 socket 的发送缓冲区中还有空间,epoll_wait
会再次报告它(即再次返回这个fd
的EPOLLOUT
事件)。
- 当
- 优点: 编程模型更简单,不容易遗漏事件。可以分多次读写数据。
- 缺点: 在极端高并发且大部分 fd 始终活跃的情况下,可能产生更多的
epoll_wait
返回和系统调用(尽管比select/poll
好很多)。如果忘记处理一个始终就绪的 fd,它会被反复报告。
边缘触发 (Edge-Triggered - ET):
- 核心原则:仅在文件描述符状态发生变化时通知你一次(例如,从不可读变为可读,从不可写变为可写)。之后,无论其状态是否持续就绪,都不会再通知,除非状态再次发生变化。
- 行为:
- 当
epoll_wait
报告一个EPOLLIN
事件(且events
包含了EPOLLET
)时,它表示有新的数据到达。如果你在这次事件处理中没有一次性把接收缓冲区里的数据全部读完,那么下次调用epoll_wait
时,即使缓冲区里还有数据,也不会再报告这个fd
的EPOLLIN
事件,直到再次有新的数据到达(状态从非可读变为可读)。 - 对于
EPOLLOUT
同理:通知一次可写后,如果你只写了一部分数据,缓冲区还没满,只要没有新的空间释放出来(即状态没有从不可写变为可写),就不会再通知。
- 当
- 要求:
- 非阻塞 I/O: 使用 ET 模式的 fd 必须 设置为非阻塞模式 (
O_NONBLOCK
)。因为你需要循环读写直到返回EAGAIN
或EWOULDBLOCK
错误,确保在本次事件触发时处理完所有就绪的数据或空间。阻塞 I/O 在最后一次读写时可能会阻塞住整个线程。 - 循环读写直到 EAGAIN: 当 ET 事件触发时,你必须立即、连续地进行读写操作,直到系统调用返回错误(
errno
为EAGAIN
或EWOULDBLOCK
),表示本次状态变化带来的所有可用数据或空间都已处理完毕。
- 非阻塞 I/O: 使用 ET 模式的 fd 必须 设置为非阻塞模式 (
- 优点:
- 减少了
epoll_wait
返回的次数和系统调用开销,尤其在长连接、活跃连接比例高的情况下效率更高。 - 避免了 LT 模式下对持续就绪 fd 的重复通知。
- 减少了
- 缺点: 编程模型更复杂,必须使用非阻塞 I/O 并确保一次性处理完所有可用数据/空间,否则可能丢失事件或造成数据饥饿。对错误处理要求更高。
- 适用场景: 需要极致性能的场景;连接长期保持活跃且数据交互频繁。
Comments NOTHING