由 Linux 内核提供的 I/O 多路复用实现,在内核中创建并维护一棵
红黑树 和一个
就绪队列(双向链表);
红黑树的增删改查时间复杂度为
O(logN),优于 select 的位图和 poll 的数组。
①
epoll_create —— 在内核中创建并维护红黑树和就绪队列:
int epollFd = epoll_create(1);
②
epoll_ctl —— 将新事件的 fd 加入红黑树或从红黑树中移除:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
ev.data.fd = clientFd;
epoll_ctl(epollFd, op, clientFd, &ev);
// op: EPOLL_CTL_ADD 添加 | EPOLL_CTL_MOD 修改 | EPOLL_CTL_DEL 删除
③
epoll_wait —— 将就绪队列中 fd 对应的事件及相关信息从内核态写到用户态的 events 数组:
struct epoll_event events[MAX_EVENTS];
int nReady = epoll_wait(epollFd, events, MAX_EVENTS, waitTime);
// nReady : 就绪事件数量
// waitTime: 超时时间(ms),-1 为永久阻塞,0 为立即返回
④
epoll_event 结构体中 events 字段的标志位:
struct epoll_event {
uint32_t events; // 事件标志位EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_data_t data; // 在 data.fd 中存入事件的 fd
};
EPOLLIN | EPOLLET | EPOLLONESHOT;
- EPOLLIN:标志可读事件,用于触发 HTTP 请求读取与解析。
- EPOLLOUT:标志可写事件,用于触发 HTTP 响应的发送。
- EPOLLET:标志边缘触发,clientFd 事件使用边缘触发,每次 HTTP 请求必须一次读完、一次写完。
- EPOLLONESHOT:将同一个 fd 的任务交给线程后,标记该 fd 暂时失效,后续该 fd 的其他任务不返回就绪队列,避免资源竞争造成死锁。
例如:客户端发送数据,该 clientFd = n 可读,主线程交给线程 A 处理;如果 n 又发送一个请求,没有 EPOLLONESHOT 将会把 n 交给线程 B 处理,此时线程 A 和 B 处理同一个 n,数据产生错乱。
⑤ 关键错误码与非阻塞设置:
// EAGAIN == EWOULDBLOCK == 11
// 表示内核读缓冲区为空 或 内核写缓冲区已满
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 数据已全部读完 / 写缓冲区满,退出循环
}
O_NONBLOCK:配合 ET 边缘触发模式,设置非阻塞标志位,当读缓冲区为空时退出循环不会阻塞线程:
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// 设置非阻塞后,read() 在无数据时立即返回 -1 并设 errno = EAGAIN