epoll 返回的是就绪事件,业务数据仍需要通过读写接口处理。本篇关注事件数组、连接缓冲区及工作分配,不把事件通知与零拷贝混为一谈。
先分清三类对象
| 对象 | 作用 | 生命周期 |
|---|---|---|
| 兴趣集合中的条目 | 记录关注的描述符及事件 | 由注册、修改和移除控制 |
| 应用的事件数组 | 接收一次 epoll_wait 返回的事件 | 可以在事件循环中复用 |
| 连接的读写缓冲区 | 保存协议数据和未发送的数据 | 跟随连接或一次任务管理 |
优化这些对象前,先明确谁拥有它们、什么时候可以复用,以及是否存在其他线程仍在访问的情况。内存池不能替代生命周期管理。
复用事件数组
下面是事件循环的片段;dispatch_event 代表应用自己的处理函数,不是一份完整服务器:
struct epoll_event events[256];
for (;;) {
int n = epoll_wait(epfd, events, 256, -1);
if (n < 0) {
if (errno == EINTR) continue;
break; // 上层应记录错误并清理实例
}
for (int i = 0; i < n; ++i) {
dispatch_event(&events[i]);
}
}
数组容量限制一次最多返回多少事件,不是整个实例最多能监听多少连接。增大数组可能减少交付批次,也可能让一轮处理耗时更长,需要结合其他连接和定时任务的响应要求选择。
如果把事件交给工作线程,不能把这个循环的数组地址当作长期有效的任务数据:下一轮 epoll_wait 会覆盖它。应复制需要的事件信息,并保证关联连接对象仍然有效。
非阻塞读取的边界
ET 模式下,通常需要读取到 EAGAIN。这个过程必须区分几种返回情况:
for (;;) {
ssize_t n = read(fd, buffer, sizeof buffer);
if (n > 0) {
consume_bytes(fd, buffer, (size_t)n); // 应用自己的协议处理
continue;
}
if (n == 0) {
close_connection(fd); // 应用负责移除监听和释放连接
break;
}
if (errno == EINTR) continue;
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
close_connection(fd);
break;
}
这里的辅助函数由应用实现。缓冲区每次读取后可能被覆盖;若协议解析或工作线程还要使用数据,就需要在覆盖前处理完或保存到由连接拥有的缓冲区。
读取到 EAGAIN 与协议消息完整是两件事。一个消息可以跨多次读取,多条消息也可以出现在一次读取中,应由协议解析器处理分帧。
写入需要保留未完成部分
非阻塞写入可能只发送部分数据。连接应保存输出缓冲区与已发送偏移;遇到 EAGAIN 后等待可写事件,再从未发送的位置继续。
当输出队列为空时,通常不再关注 EPOLLOUT,避免反复收到当前不需要处理的可写通知。重新产生输出数据时再恢复相应的关注事件。
这类状态管理比“是否使用边缘触发”更基础。丢失偏移、过早复用缓冲区或关闭后继续处理事件,都可能让程序在高负载下出现错误。
工作线程与背压
事件循环适合做有界的事件处理。较长的业务任务可以转交工作线程,但还需要限制队列容量和同一连接的并发处理。
- 入队前明确事件信息和连接对象的所有权。
- 队列满时采取明确的背压策略,避免无界堆积。
- 使用
EPOLLONESHOT时,在处理完成后由负责方重新激活;它不负责应用对象的释放与所有并发问题。 - 描述符关闭后可能被系统复用,任务不能只凭一个 fd 整数就认定连接身份仍然相同。
这些规则帮助控制资源规模,但具体线程数量、任务时长和队列长度仍需要测量。
零拷贝属于另一层
原文将 epoll 的优化解释为 mmap 共享就绪链表,这不符合实际实现,因此移除了相应“源码”和性能对比。epoll_wait 将事件交付到用户缓冲区;eventpoll_fops 没有原文所写的 mmap 操作。内核源码
文件到网络的传输可以另行研究 sendfile,其他路径可以研究 splice。它们的适用条件、数据来源和错误处理不同,不能概括为“用了 epoll 或 io.Copy 就没有数据拷贝”。
用什么验证优化
先验证行为,再比较性能:
- 用小消息、分段消息和连接中途关闭检查读写状态。
- 增加连接数量,同时区分空闲连接与活跃连接比例。
- 记录队列长度、缓冲区占用、事件循环处理时间及尾延迟。
- 在相同环境和负载下比较调整前后,保留命令与原始输出。
本篇没有给出实测吞吐数字。数组复用、缓冲区复用和工作分配各自解决不同问题,性能收益需要在自己的负载中确认。
触发方式、非阻塞读写和 EPOLLONESHOT 的语义可进一步查阅 epoll 手册。