加载中...

[epoll] EP3 事件循环、缓冲区与工作分配

  • 4分钟
[epoll] EP3 事件循环、缓冲区与工作分配

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 就没有数据拷贝”。

用什么验证优化

先验证行为,再比较性能:

  1. 用小消息、分段消息和连接中途关闭检查读写状态。
  2. 增加连接数量,同时区分空闲连接与活跃连接比例。
  3. 记录队列长度、缓冲区占用、事件循环处理时间及尾延迟。
  4. 在相同环境和负载下比较调整前后,保留命令与原始输出。

本篇没有给出实测吞吐数字。数组复用、缓冲区复用和工作分配各自解决不同问题,性能收益需要在自己的负载中确认。

触发方式、非阻塞读写和 EPOLLONESHOT 的语义可进一步查阅 epoll 手册。

更新于 2026-10-05
播放列表
当前歌曲封面
点击加载歌单
-