「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
前言
接触这几个概念是在整理BIO和NIO的时候,开始只知道epoll和select都是I/O多路复用的技术,都可以实现同时监听多个I/O事件的状态。只知道select是轮询模式效率较低,epoll是事件监听模式效率更高,底层基于linux,其实也是云里雾里,特此出此系列文章作为自己进一步学习和整理。(笔者技术栈有限,linux底层不会涉及太深只是对概念/机制基本的了解。)
上篇:Linux之poll
确定学习目标
- epoll的本质是啥
epoll
名称
epoll - I/O 事件通知机制
概要
#include <sys/epoll.h>
描述
epoll API执行与poll(2)类似的任务:监视多个文件描述符,以查看它们中是否存在I/O。epoll API可以用作边沿触发或水平触发接口并且可以很好地扩展到大量监视的文件描述符。
epoll API的核心概念是epoll实例,这是一种内核数据结构,从用户空间的角度来看,它可以被视为两个列表的容器:
- interest list(有时也称为epoll set):进程已注册并有兴趣监视的文件描述符集。
- ready list:准备好I/O的文件描述set。就绪列表是兴趣列表中文件描述符的子集(或更准确地说,引用set)。文件描述符上有I/O活跃,内核就会动态填充就绪列表。
提供以下系统调用来创建和管理 epoll 实例:
-
epoll_create(2) 创建一个新的 epoll 实例并返回一个引用该实例的文件描述符。 (最近的 epoll_create1(2) 扩展了 epoll_create(2) 的功能。)
-
通过epoll_ctl(2)对感兴趣的特定文件描述符进行注册,添加到epoll实例的interest list。
-
epoll_wait(2) 等待 I/O 事件,如果当前没有可用的事件,则阻塞调用线程。 (这个系统调用可以被认为是从 epoll 实例的ready list中获取项目。)
水平触发和边沿触发
epoll事件分发接口既可以作为边沿触发(ET),也可以作为水平触发(LT)。两种机制之间的区别可以描述如下。假设发生这种情况:
- 代表pipe读取端的文件描述符(rfd)注册在epoll实例上。
- pipe写入器在pipe的写入端写入2kb的数据。
- 调用epoll_wait(2)将返回rdf作为准备好的文件描述符。
- pipe阅读器从rdf读取1kB的数据。
- 对 epoll_wait(2) 进行调用。
如果已使用 EPOLLET(边沿触发)标志将 rfd 文件描述符添加到 epoll 接口,则在步骤 5 中对 epoll_wait(2) 的调用可能会挂起,尽管文件输入缓冲区中仍然存在可用数据;同时,远程对等点可能期待基于它已经发送的数据的响应。这样做的原因是边沿触发模式仅在受监视的文件描述符发生更改时才传递事件。因此,在第 5 步中,调用者可能一直会等待输入缓冲区中已经存在的一些数据。在上面的例子中,由于 2 中的 write done 会在 rfd 上产生一个事件,并且该事件在 3 中被消费。由于 4 中完成的读操作没有消耗整个缓冲区数据,所以对 epoll_wait(2) 的调用 在第 5 步中可能会无限期阻塞。
使用 EPOLLET 标志的应用程序应使用非阻塞文件描述符以避免阻塞读取或写入使处理多个文件描述符的任务饿死。使用 epoll 作为边沿触发 (EPOLLET) 接口的建议方法如下:
a) 使用非阻塞文件描述符。
b) 只有在read(2)或write(2)返回EAGAIN事件后才等待。
相比之下,当用作水平触发接口时(默认情况下,未指定 EPOLLET 时),epoll只是一个更快的poll(2),并且可以在使用后者的任何地方使用,因为它具有相同的语义。
由于即使使用边沿触发的epoll,在接收到多个数据块时可以生成多个事件,调用者可以选择指定 EPOLLONESHOT 标志,告诉 epoll 在接收到带有 epoll_wait(2) 的事件后禁用关联的文件描述符。 当指定 EPOLLONESHOT 标志时,调用者有责任使用 epoll_ctl(2) 和 EPOLL_CTL_MOD 重新配置文件描述符。
如果多个线程(或进程,如果子进程通过 fork(2) 继承了 epoll 文件描述符)在 epoll_wait(2) 中被阻塞,等待同一个 epoll 文件描述符和在interest list中标记为边沿触发触发等待就绪的文件描述符,只有一个线程(或进程)从 epoll_wait(2) 中唤醒。 这为在某些情况下避免“惊群效应”唤醒提供了有用的优化。
与autosleep的交互
如果系统通过 /sys/power/autosleep 处于自动睡眠模式,并且发生将设备从睡眠中唤醒的事件,则设备驱动程序将仅在该事件排队之前保持设备唤醒。 要在处理完事件之前保持设备唤醒,必须使用 epoll_ctl(2) EPOLLWAKEUP 标志。
当 EPOLLWAKEUP 标志在 struct epoll_event 的 events 字段中设置时,系统将从事件排队的那一刻起保持清醒,通过 epoll_wait(2) 调用返回事件,直到随后的 epoll_wait(2) 调用。 如果事件应该使系统保持清醒超过该时间,则应在第二次 epoll_wait(2) 调用之前获取单独的 wake_lock。
/proc 接口
以下接口可用于限制 epoll 消耗的内核内存量:
/proc/sys/fs/epoll/max_user_watches(自 Linux 2.6.28 起)这指定了用户可以在系统上的所有 epoll 实例中注册的文件描述符总数的限制。 限制是每个真实用户 ID。每个注册的文件描述符在 32 位内核上大约需要 90 个字节,在 64 位内核上大约需要 160 个字节。目前,max_user_watches 的默认值是 1/25 (4%) 可用的低内存,除以注册成本(以字节为单位)。
版本
epoll API 是在 Linux 内核 2.5.44 中引入的。 在 2.3.2 版本中添加了对 glibc 的支持。
小结
-
epoll为事件通知模式。
-
epoll分为边沿触发模式和水平触发模式。
- 边沿触发模式(ET):和LT模式不同的是,通知之后进程必须立即处理事件,下次再调用epoll_wait()时不会再得到事件到达的通知。很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。只支持No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
- 水平触发模式(LT):当epoll_wait()检测到描述符事件达到时,将此事件通知进程,进程可以不立即处理该事件,下次调用epoll_wait()会再次通知进程。是默认的一种模式,并且同时支持Blocking和No-Blocking。
-
epoll分为两个list:interest list、ready list。
-
epoll_create创建一个新的epoll实例;epoll_ctl对感兴趣的fd进行注册添加到interest list中。epoll_wait等待I/O事件。
-
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
-
epoll通过内核和用户空间共享一块内存来实现的
epoll本质
使用事件通知模式,不用遍历所有fd找到就绪的流进行操作。复杂度也降到O(1)。