epoll 如何高效工作

243 阅读12分钟

概念

epoll 是一种 I/O 事件通知机制,它是 Linux 内核中 IO 多路复用的实现。 IO 多路复用意味着在单个操作中同时侦听多个输入和输出源,当其中一个或多个可用时返回,然后对它们执行读取和写入操作。

I/O

输入/输出对象可以是进程之间的文件、套接字和管道。在 Linux 系统中,这些由文件描述符 (fd) 表示。

事件

  • 当与文件描述符关联的内核读取缓冲区可读时,将触发可读事件。(可读:内核缓冲区不为空,有数据要读取)
  • 可写事件,当与文件描述符关联的内核写入缓冲区可写时触发。(可写:内核缓冲区不为空,有可用空间写入)

通知机制

通知机制,即在事件发生时,然后主动通知。通知机制的另一面是轮询机制。

通俗地说epoll

结合以上三项,epoll 是一种机制,当文件描述符的内核缓冲区不为空时,通过发送可读信号通知内核,当写入缓冲区已满时通过发送可写信号通知内核

epoll的接口

epoll 的核心是三个 API,核心数据结构是:红黑树和链表。

电子波尔 API

1. 整数epoll_create(整数大小)

功能。

  • 内核生成 epoll 实例数据结构并返回文件描述符。这个特定的描述符是 epoll 实例的句柄,后面的两个接口以它为中心(即 epfd 形式参数)。

size 参数指示要监视的文件描述符的最大值,但在更高版本的 Linux 中已被弃用(另外,不要为 size 传递 0,它将报告无效的参数错误)

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

功能。

  • 添加正在侦听的描述符或将其从红黑树中删除,或修改要监视侦听事件的文件描述符的最大值,尽管这在更高版本的 Linux 中已被弃用(此外,不要传递 0 表示大小,将报告无效的参数错误)
typedef union epoll_data {
    void *ptr; /* 指向用户自定义数据 */
    int fd; /* 注册的文件描述符 */
    uint32_t u32; /* 32-bit integer */
    uint64_t u64; /* 64-bit integer */
} epoll_data_t;

struct epoll_event {
    uint32_t events; /* 描述epoll事件 */
    epoll_data_t data; /* 见上面的结构体 */
};

对于要监视的文件描述符集,epoll_ctl管理红黑树,其中每个成员由描述符值和对要监视的文件描述符指向的文件表条目的引用组成,等等。

op 参数描述操作的类型。

  • EPOLL_CTL_ADD:将描述符添加到需要监视的兴趣列表
  • EPOLL_CTL_DEL:从兴趣列表中删除描述符
  • EPOLL_CTL_MOD:修改兴趣列表中的描述符

结构epoll_event描述文件描述符的 epoll 行为。使用 epoll_wait 函数返回处于就绪状态的描述符列表时,

  • 数据字段是唯一提供有关描述符信息的字段,因此在调用epoll_ctl添加需要监视的描述符时,请确保在此字段中写入有关描述符的信息
  • 事件字段是一个位掩码,用于描述一组 epoll 事件,在epoll_ctl调用中将其解释为:描述符预期的 epoll 事件,可以多选。

常用的 epoll 事件描述如下。

  • EPOLLIN:描述符处于可读状态
  • EPOLLOUT:描述符处于可写状态
  • EPOLLET:将 epoll 事件通知模式设置为边缘触发
  • EPOLLONESHOT:第一时间通知,之后不再监控
  • EPOLLHUP:本地描述符生成挂断事件,默认监控事件
  • EPOLLRDHUP:相反的描述符生成一个挂起的事件
  • EPOLLPRI:由带外数据触发
  • EPOLLERR:当描述符生成错误时触发,默认检测事件

3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

功能。

  • 阻塞 等待注册事件发生,返回事件数,并将触发的事件写入事件数组。
  • 事件:用于记录触发的事件,其大小应与最大事件相同
  • 最大事件数:要返回的最大事件数

处于就绪状态的文件描述符将复制到就绪列表中,epoll_wait用于将就绪列表返回到用户进程。Events 和 MaxEvents 参数描述了用户分配的结构 epoll 事件数组,当调用返回时,内核将 ready 列表复制到此数组并返回它。当调用返回时,内核将就绪列表复制到此数组中,并返回实际副本数作为返回值。请注意,如果就绪列表比 maxevents 长,则只能复制第一个 maxevents 成员;相反,可以完全复制就绪列表。 此外,结构 epoll 事件结构中的事件字段在此处解释为在被监视的文件描述符上发生的实际事件。 参数超时描述函数调用中阻塞时间的上限(以毫秒为单位)。

  • 超时 = -1 表示调用将继续阻塞,直到文件描述符进入就绪状态或在返回之前捕获信号。
  • timeout = 0 用于非阻塞,以检测描述符是否处于就绪状态,并且无论结果如何,调用都会立即返回。
  • 超时 > 0 表示调用将持续最多超时时间,如果检测到的对象更改为就绪状态或在此期间捕获信号,则返回,否则直到超时。

epoll的两种触发方式

epoll 监视多个文件描述符的 I/O 事件。epoll 支持边缘触发器 (ET) 或电平触发器 (LT),它们通过 epoll_wait 等待 I/O 事件,如果当前没有可用事件,则阻止调用线程。

select和poll仅支持 LT 工作模式,epoll 的默认工作模式为 LT 模式。

1. 水平触发的定时

  1. 对于读取操作,只要缓冲区不为空,LT 模式就会返回“读取就绪”。2. 对于写入操作,只要缓冲区不为空,LT 模式就会返回读取就绪状态。
  2. 对于写入操作,只要缓冲区仍已满,LT 模式就会返回写入就绪状态。

当受监视的文件描述符上发生读/写事件时,epoll_wait() 会通知处理程序读取或写入。如果你没有一次读取或写入所有数据(例如,读/写缓冲区太小),那么下次调用 epoll_wait() 时,它会通知你继续读取或写入你没有完成读取或写入的文件描述符,但当然如果你从未读取或写入, 它会一直通知您。如果系统有大量不需要读取或写入的就绪文件描述符,并且它们每次都返回,这可能会大大降低处理程序检索它所关心的就绪文件描述符的效率。

2. 边沿触发的定时

  • 用于读取操作

    1. 当缓冲区从不可读变为可读时,即当缓冲区从空变为非空时。
    2. 当新数据到达时,即当缓冲区充满要读取的数据时。
    3. 当缓冲区中有可读数据并且应用程序进程执行EPOLL_CTL_MOD以修改相应描述符的 EPOLLIN 事件时。
  • 对于写入操作

    1. 当缓冲区从不可写更改为可写时。
    2. 当有旧数据被发送走时,即缓冲区变得较少的内容。
    3. 当缓冲区中有要写入的空间并且应用程序进程在相应的描述符上执行EPOLL_CTL_MOD修改 EPOLLOUT 事件时。

当受监视的文件描述符上发生读写事件时,epoll_wait() 会通知处理程序读取和写入。如果它这次没有读取或写入所有数据(例如,读/写缓冲区太小),那么下次调用 epoll_wait() 时,它不会通知您,即它只会通知您一次,直到该文件描述符上发生第二个读/写事件。此模式比水平触发更有效,并且系统不会充斥着大量您不关心的现成文件描述符。

在 ET 模式下,缓冲区从不可读变为可读,这会唤醒应用程序进程,如果缓冲区数据变低,则不会再次唤醒应用程序进程。

例 1.

  1. 读取缓冲区在开始时为空
  2. 2KB 的数据写入读取缓冲区
  3. 水平触发和边沿触发模式此时都会发出可读信号
  4. 收到信号通知后,读取 1KB 的数据,读取缓冲区中保留 1KB 的数据
  5. 水平触发器将再次通知,而边缘触发器不会再次通知

例2:(以脉冲的高低电平为例)

  • 水平触发:0 表示无数据,1 表示数据。如果缓冲区中有数据,它将始终为 1,然后它将始终触发。
  • 边触发毛发:0 表示无数据,1 表示数据,只要上升沿 0 变为 1 就触发。

JDK 没有实现边缘触发,Netty 重新实现了 epoll 机制,使用边缘触发;也像Nginx也使用边缘触发。

JDK 在 Linux 中已经默认使用 epoll,但 JDK epoll 使用水平触发,而 Netty 使用边缘触发重新实现了 epoll 机制。Netty epoll 传输公开了更多nio没有的配置参数,如TCP_CORK、SO_REUSEADDR等;像Nginx这样的其他也使用边缘触发。

epoll与select和poll

1. 将文件描述符传递到内核的用户状态方式

  • select:创建 3 组文件描述符并将它们复制到内核,分别侦听读取、写入和异常操作。这受到单个进程可以打开的 fd 数量的限制,默认值为 1024。
  • poll:将结构 pollfd 结构的传入数组复制到内核进行侦听。
  • epoll:执行epoll_create在内核的高速缓存区域中创建一个红黑树和一个就绪链接列表(存储已经准备好的文件描述符)。然后用户执行的添加文件描述符的epoll_ctl函数会将相应的节点添加到红黑树中。

2. 文件描述符读/写状态的内核状态检测

  • select:轮询,遍历所有 FD,并返回描述符是否已准备好进行读/写操作的掩码,并基于此掩码为fd_set分配值。
  • poll:还是轮询,查询每个 FD 的状态,将项目添加到等待队列(如果准备就绪),并继续迭代。
  • epoll:使用回调机制。在执行 epoll_ctl 的 add 操作时,它不仅将文件描述符放在红黑树中,而且还注册了一个回调函数。内核在检测到文件描述符可读/可写时将调用回调函数,回调函数会将文件描述符放在就绪链接列表中。

3. 找到准备好的文件描述符并将它们传递给用户状态

  • select:将以前传递的fd_set的副本传递给用户状态,并返回就绪的文件描述符总数。用户状态不知道哪些文件描述符处于就绪状态,需要循环访问它们。
  • poll:将以前传入的 fd 数组的副本传递给用户状态,并返回准备就绪的文件描述符总数。用户状态不知道哪些文件描述符处于就绪状态,需要遍历以确定这一点。
  • epoll:epoll_wait只是监视 ready linkedlist 中的数据,最后将 linkedlist 返回到数组和就绪文件的数量。内核将准备好的文件描述符放在传入数组中,因此您可以按顺序遍历它们。此处返回的文件描述符由 mmap 传递,允许内核和用户空间共享相同的内存块,从而减少不必要的副本。

4. 重复监听器的处理

  • select:将一组新的侦听文件描述符的副本传递到内核中,然后继续执行上述步骤。
  • poll:将新结构 pollfd 数组的副本传递到内核中,然后继续执行上述步骤。
  • epoll:不需要重建红黑树,只要遵循现有的树。

epoll更高效的原因

  1. Select 和 poll 的操作基本相同,只是 poll 使用 linkedlist 进行文件描述符存储,而 select 使用 FD 标记的位进行存储,因此 select 受最大连接数的限制,而 poll 则不受限制。
  2. select、poll和 epoll 都返回就绪文件描述符的数量。但是select和poll并没有明确指示哪些文件描述符已经准备好,而 epoll 会。不同的是,在系统调用返回后,调用 select 和 poll 的程序需要遍历整个侦听器文件描述符,找出谁准备好了,而 epoll 可以直接处理。
  3. select和poll都需要将文件描述符的数据结构复制到内核中,然后将其复制出来。epoll 创建的文件描述符的数据结构本身存储在内核状态中,系统调用返回 mmap() 文件映射内存,以加快消息传递到内核空间的速度:也就是说,epoll 使用 mmap 来减少复制开销。
  4. select和poll 使用轮询来检查文件描述符是否处于就绪状态,而 epoll 使用回调机制。结果是,select和poll的效率随着 fd 的增加而线性降低,而 epoll 不会受到太大影响,除非有许多活动套接字。
  5. epoll 的边缘触发模式非常高效,系统不会充斥着不关心的文件描述符

虽然 epoll 的性能最好,但当连接数较少且连接都非常活跃时,select 和 poll 的性能可能比 epoll 更好,毕竟 epoll 的通知机制需要很多函数回调。