一、epoll 核心函数详解
1. epoll_create
/epoll_create1
- 创建 epoll 实例
c
#include <sys/epoll.h>
int epoll_create(int size); // Linux 2.6.8前需指定size(>=1),后续版本可忽略
int epoll_create1(int flags); // 推荐使用,支持flags参数
- 功能:创建一个 epoll 实例,返回文件描述符(epfd),用于管理监视的文件描述符。
- 参数:
size
:旧版本需指定预分配大小(无实际作用,可设为 1)。flags
:0
:与epoll_create
功能相同。EPOLL_CLOEXEC
:使 epfd 在进程 fork 时自动关闭。
- 返回值:成功返回 epfd(非负整数),失败返回
-1
并置errno
。 - 示例:
c
int epfd = epoll_create1(EPOLL_CLOEXEC); // 创建带自动关闭功能的epoll实例
2. epoll_ctl
- 管理监视列表
c
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 功能:向 epoll 实例中添加、修改或删除监视的文件描述符及其事件。
- 参数:
epfd
:epoll 实例的文件描述符。op
:操作类型:EPOLL_CTL_ADD
:添加文件描述符fd
到监视列表。EPOLL_CTL_MOD
:修改fd
关联的事件。EPOLL_CTL_DEL
:从监视列表删除fd
(event
参数可置NULL
)。
fd
:需操作的文件描述符(如套接字)。event
:指向epoll_event
结构体的指针,包含监视事件和用户数据:c
struct epoll_event { uint32_t events; // 监视事件(如EPOLLIN、EPOLLOUT、EPOLLET) epoll_data_t data; // 用户数据(如fd、指针等) }; typedef union epoll_data { void *ptr; // 自定义指针 int fd; // 监视的文件描述符 uint32_t u32; // 32位整数 uint64_t u64; // 64位整数 } epoll_data_t;
- 返回值:成功返回
0
,失败返回-1
并置errno
。 - 示例:
c
struct epoll_event event; event.events = EPOLLIN | EPOLLET; // 边缘触发模式 + 读事件 event.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event); // 添加套接字到监视列表
3. epoll_wait
- 等待事件触发
c
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 功能:阻塞等待 epoll 实例中监视的文件描述符触发事件,返回激活的事件列表。
- 参数:
epfd
:epoll 实例的文件描述符。events
:用于存储激活事件的结构体数组。maxevents
:events
数组的最大长度(需小于等于创建时的预分配值)。timeout
:超时时间(ms):-1
:永久阻塞,直到事件触发。0
:非阻塞,立即返回。>0
:超时时间,到期后返回。
- 返回值:
>0
:激活的事件数量(events
中有效元素个数)。0
:超时且无事件触发。-1
:失败并置errno
。
- 示例:
c
struct epoll_event events[1024]; int n = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1); // 永久阻塞等待事件 for (int i = 0; i < n; i++) { int fd = events[i].data.fd; // 处理事件... }
二、epoll 工作原理
- 数据结构:
- 监视列表:使用红黑树存储所有监视的文件描述符,支持快速增删改查(时间复杂度 O (logN))。
- 激活列表:使用链表或动态数组存储当前触发事件的文件描述符,避免遍历整个红黑树。
- 事件触发流程:
- 通过
epoll_ctl
将文件描述符添加到红黑树(监视列表)。 - 内核监听文件描述符状态,当事件(如可读、可写)发生时,将其加入激活列表。
epoll_wait
返回激活列表中的事件,用户空间处理后,激活列表清空。
- 通过
三、LT(水平触发)与 ET(边缘触发)模式对比
特性 | LT(水平触发) | ET(边缘触发) |
---|---|---|
触发条件 | 只要内核缓冲区有数据未读 / 未写,就持续触发事件。 | 仅在状态变化时触发一次事件(如数据从无到有)。 |
读操作 | 允许分多次读取,剩余数据会再次触发事件。 | 必须一次性读完所有数据(需配合非阻塞 I/O),否则剩余数据不再触发。 |
写操作 | 只要内核缓冲区有空间,就持续触发可写事件。 | 仅在缓冲区由满变空时触发一次可写事件。 |
编程复杂度 | 低,类似 select/poll,无需处理非阻塞。 | 高,必须使用非阻塞 I/O,避免阻塞导致事件丢失。 |
适用场景 | 简单场景、低速设备或不确定数据量的场景。 | 高并发、高速数据传输场景(如 Web 服务器、实时通信)。 |
- 示例配置:
c
// LT模式(默认,无需指定EPOLLET) event.events = EPOLLIN; // ET模式(需显式指定EPOLLET) event.events = EPOLLIN | EPOLLET;
四、select/poll/epoll 对比(面试重点)
特性 | select | poll | epoll |
---|---|---|---|
数据结构 | 位图(数组),最大连接数受限于FD_SETSIZE (默认 1024)。 |
结构体数组,理论无连接数限制(仅受内存限制)。 | 红黑树 + 激活列表,支持上万级连接(如 10,000+)。 |
内核遍历方式 | 每次调用遍历所有监视的文件描述符(O (n))。 | 每次调用遍历所有监视的文件描述符(O (n))。 | 仅遍历激活列表中的文件描述符(O (1)~O (k),k 为激活数)。 |
用户态 / 内核态数据拷贝 | 每次select 需拷贝全量文件描述符到内核。 |
每次poll 需拷贝全量文件描述符到内核。 |
仅在epoll_ctl 时更新红黑树,epoll_wait 无需拷贝。 |
触发模式 | 仅支持 LT 模式。 | 仅支持 LT 模式。 | 支持 LT 和 ET 模式。 |
跨平台性 | 支持(Linux/Windows/macOS)。 | 支持(Linux/UNIX)。 | 仅 Linux 支持。 |
适用场景 | 低并发、跨平台场景(如嵌入式)。 | 中等并发场景。 | 高并发、Linux 服务器场景(如 Nginx、Redis)。 |
五、epoll 服务器示例代码关键点(C 语言)
c
// 1. 创建epoll实例并设置监听套接字为非阻塞
int epfd = epoll_create1(EPOLL_CLOEXEC);
set_nonblocking(listen_fd); // 自定义函数,通过fcntl设置O_NONBLOCK
// 2. 注册监听套接字到epoll(ET模式)
struct epoll_event event = {
.events = EPOLLIN | EPOLLET,
.data = {.fd = listen_fd}
};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event);
// 3. 处理新连接(ET模式下需循环accept直到EAGAIN)
if (fd == listen_fd) {
while (1) {
int connfd = accept(listen_fd, &cliaddr, &addrlen);
if (connfd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 无新连接
perror("accept");
exit(EXIT_FAILURE);
}
set_nonblocking(connfd); // 客户端套接字也需设为非阻塞
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event); // 注册读事件(ET模式)
}
}
// 4. 处理读事件(ET模式下需循环recv直到EAGAIN)
else {
char buf[BUFFER_SIZE];
while (1) {
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 数据读完
perror("recv");
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
} else if (n == 0) { // 客户端关闭连接
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
}
// 处理数据...
}
}
六、总结
- epoll 优势:
- 高并发:基于红黑树和激活列表,适合管理上万级连接。
- 高性能:事件通知复杂度 O (1),减少 CPU 消耗。
- 灵活性:支持 LT/ET 模式,适配不同场景。
- 使用建议:
- 高并发服务器首选 epoll,配合 ET 模式和非阻塞 I/O。
- 简单场景或跨平台需求使用 select/poll。
- 注意设置文件描述符非阻塞,避免 ET 模式下的数据丢失。