type
status
slug
summary
tags
category
icon
password
new update day
Property
Oct 22, 2023 01:31 PM
created days
Last edited time
Oct 22, 2023 01:31 PM

Linux epoll 机制

0 同步I/O,异步I/O

  • 同步(阻塞)I/O:在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
  • 异步(非阻塞)I/O:当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
💡
Linux epoll 机制是一种 I/O 事件通知机制,它可以用来监听多个文件描述符上的事件。相比于传统的 select 和 poll 机制,epoll 更加灵活,具有更高的效率和更好的扩展性,可以处理更多的并发连接。
epoll 机制的核心是 epoll_create 函数,它创建了一个 epoll 对象,用于存储文件描述符和事件信息。epoll 对象可以通过 epoll_ctl 函数来添加、修改和删除文件描述符和事件信息。当文件描述符上有事件发生时,epoll_wait 函数会返回事件信息,应用程序可以根据事件信息进行相应的处理。

1 epoll 基本概念

epoll 机制中有三个重要的概念:epoll 文件描述符、事件和事件源。

1.1 epoll 文件描述符

epoll 文件描述符是 epoll 的核心,它是一个指向内核数据结构的指针,用于管理事件源和事件。应用程序通过 epoll 文件描述符与内核进行交互,可以注册、修改和删除事件源,以及等待事件的发生。

1.2 事件

事件是 epoll 中的一个重要概念,它表示一个文件描述符上的某个事件。epoll 支持的事件包括:
  • EPOLLIN: 文件描述符可读
  • EPOLLPRI: 优先级数据可读
  • EPOLLOUT: 文件描述符可写
  • EPOLLRDNORM: 文件描述符可读
  • EPOLLRDBAND: 优先级数据可读
  • EPOLLWRNORM: 文件描述符可写
  • EPOLLWRBAND: 优先级数据可写
  • EPOLLMSG: 消息可读
  • EPOLLERR: 文件描述符发生错误
  • EPOLLHUP: 文件描述符被挂起
  • EPOLLRDHUP: 对端关闭了连接
  • EPOLLEXCLUSIVE: 独占模式
  • EPOLLWAKEUP: 唤醒模式
  • EPOLLONESHOT: 一次性事件
  • EPOLLET: 边缘触发模式
EPOLL_EVENTS 的源码定义
 

1.3 事件源

事件源是 epoll 中的另一个重要概念,它表示一个文件描述符。事件源可以是一个 socket、一个文件、一个管道等等。

2 epoll api

epoll api 包括三个函数:epoll_create、epoll_ctl 和 epoll_wait。

2.1 epoll_create

epoll_create 函数用于创建一个 epoll 文件描述符,它的原型如下:
其中,size 表示 epoll 文件描述符管理的事件源数量上限,它并不是一个硬性限制,只是一个提示。如果 size 小于等于 0,epoll_create 会返回一个错误。
调用该 API 后,操作系统内核会产生一个 eventpoll 实例的数据结构并返回一个 fd,这个 fd 就是 epoll 实例的句柄,下面的两个 API 都以它为中心。

2.2 epoll_ctl

epoll_ctl 函数用于注册、修改和删除事件源,它的原型如下:
其中,epfd 是 epoll 文件描述符,op 表示操作类型,可以是 EPOLL_CTL_ADDEPOLL_CTL_MODEPOLL_CTL_DELfd 是事件源的文件描述符,event 是一个 epoll_event 结构体,用于描述事件类型和事件源。

2.3 epoll_wait

epoll_wait 函数用于等待事件的发生,它的原型如下:
其中,epfd 是 epoll 文件描述符,events 是一个 epoll_event 数组,“events”参数是一个缓冲区,将包含触发的事件(用于存储发生的事件),“maxevents” 是要返回的最大事件数(通常是 “events” 的大小),timeout 表示等待事件的超时时间(-1 == 无限),单位是毫秒。
  • 如果 timeout 为 -1,表示一直等待,直到有事件发生;
  • 如果 timeout 为 0,表示立即返回,不等待事件的发生;
  • 如果 timeout 大于 0,表示等待 timeout 毫秒,如果在这个时间内有事件发生,就返回;否则超时返回 0。
等待 epoll 实例 "epfd" 上的事件。返回在 "events" 缓冲区中返回的触发事件的数量。如果出现错误,则返回 -1,并将 "errno" 变量设置为特定的错误代码。
以下是 epoll 相关函数的定义和内容详解:

3 触发机制

epoll 有三种触发机制:LT(Level Triggered)、ET(Edge Triggered)和 ONESHOT。

1、水平触发:level trigger(LT)

LT(Level Triggered)是 epoll 的默认触发机制。当 epoll_wait() 检测到文件描述符上有事件发生,epoll_wait就会以非阻塞的方式返回,并将此事件通知应用程序后,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,它还会再次通知应用程序该事件。
  • 这种触发机制的优点是应用程序可以不必处理所有事件,只处理它感兴趣的事件。
  • 缺点是如果应用程序处理事件的速度太慢,该epoll事件没有被处理完(没有返回 EWOULDBLOCK ),那么 epoll_wait() 将不断通知应用程序同一个事件,这将导致 CPU 占用率过高。

2、边缘触发:edge trigger(ET)

ET(Edge Triggered)是一种更高效的触发机制。
epoll_wait() 检测到文件描述符上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件(立即返回,并且sleep这一事件的epoll_wait,不管该事件有没有结束)。如果不处理,下次调用 epoll_wait() 时将不会再次通知应用程序该事件
  • 这种触发机制的优点是可以避免 epoll_wait() 不断通知同一个事件,从而降低 CPU 占用率。
  • 缺点是应用程序必须立即处理事件,否则将会丢失事件。
在使用ET模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用readwrite的时候都必须等到它们返回EWOULDBLOCK(确保所有数据都已读完或写完)。

3、ONESHOT

ONESHOT 触发机制是一种特殊的 ET 触发机制。当 epoll_wait() 检测到文件描述符上有事件发生并将此事件通知应用程序后,应用程序 必须调用 epoll_ctl() 函数重新注册该文件描述符 ,否则下次调用 epoll_wait() 时将不会再次通知应用程序该事件。
  • 这种触发机制的优点是可以避免 epoll_wait() 不断通知同一个事件,从而降低 CPU 占用率。
  • 缺点是应用程序必须重新注册文件描述符,否则将会丢失事件。

4 epoll 与 select 和 poll 对比

select 和 poll 是 Linux 下的两种 I/O 多路复用机制,相比于 epoll,它们在用户态和内核态的切换上更加耗时,同时还存在文件描述符数量限制的问题。

1、文件描述符数量

  • select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
  • poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
  • epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效

2、将 fd 传入内核的方式

  • select:从用户态创建拷贝到内核态,每次调用都需要拷贝
  • poll:从用户态拷贝到内核态,每次调用都需要拷贝
  • epoll:通过 epoll_create 直接在内核态创建一棵红黑树,通过 epoll_ctl 将要监听的文件描述符注册到红黑树上。

3、内核态检测 fd 就绪状态的方式

  • select:轮询机制遍历所有的 fd,判断哪个文件描述符上有事件发生
  • poll:轮询机制遍历所有的 fd,判断哪个文件描述符上有事件发生
  • epoll:回调机制,调用 epoll_ctl 时会在内核态注册回调函数,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后 epoll_ctl 传来的 fd 外,还会再建立一个list链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可。
    • epoll 是根据每个 fd 上面的回调函数(中断函数)判断,只有发生了事件的 socket 才会主动的去调用 callback 函数,其他空闲状态 socket 则不会,若是就绪事件,插入 list。

4、应用程序索引就绪文件描述符

  • select/poll 只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历。
  • epoll 返回的发生了事件的个数和结构体数组,结构体包含 socket 的信息,因此直接处理返回的数组即可。

5、工作模式

  • select 和 poll 都只能工作在相对低效的 LT 模式下
  • epoll 则可以工作在 ET 高效模式,并且 epoll 还支持 EPOLLONESHOT 事件,该事件能进一步减少可读、可写和异常事件被触发的次数。

5 epoll 更高效的原因

  • 对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll 则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销是很大的,而且在有很多短期活跃连接的情况下,epoll 可能会慢于 select 和 poll 由于这些大量的系统调用开销。
  • select 和 poll 的动作基本一致,只是 poll 采用链表的方式来存储 fd,而 select 采用 fd 标注位来存放,所以 select 会受到最大连接数的限制而 poll 不会。epoll 底层通过红黑树来描述,并且维护一个 ready list,将事件表中已经就绪的事件添加到这里,在使用 epoll_wait 调用时,仅观察这个 list 中有没有数据即可。
  • select、poll、epoll 虽然都会返回就绪的 fd 数量,但是 select 和 poll 并不会明确指出是哪些 fd 就绪,而 epoll 会。这造成的区别就是:系统调用返回后,调用 select 和 poll 的程序需要遍历监听的整个文件描述符找到是哪些处于就绪状态,产生了大量的开销,而 epoll 则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。随着 fd 数量的增加,select 和 poll 的效率会越来越低,而 epoll 则不会受到太大影响。
  • select 和 poll 都只能工作在相对低效的LT模式下,而 epoll 同时支持 LT 和 ET 模式。epoll 的 ET 模式效率高,系统不会充斥大量不关心的就绪 fd。

6 具体应用场景

  • 当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如 selece 和 poll。
  • 当监测的 fd 数目较小,且各个 fd 都比较活跃,建议使用 select 或者 poll。
  • 当监测的 fd 数目非常大,成千上万,且单位时间只有其中的一部分 fd 处于就绪状态,这个时候使用 epoll 能够明显提升性能。

参考资料

Build ollvm yourselfWindows 11 启用 DOH (基于阿里私人免费 DNS)