epoll

前言

epoll 是一种 I/O 多路复用技术,主要用于高性能的网络服务器中,特别是在处理大量并发连接时。它是 Linux 特有的,自 Linux 内核 2.5.44 版本引入,并在后续版本中不断优化。epoll 能够帮助服务器高效地管理数以千计的客户端连接,是 selectpoll 方法的现代替代品。

本文不对 epoll 的源码进行分析,仅做原理上的总结,方便快速查阅回顾。各大论坛很多大佬都对 epoll 的源码进行了详尽的分析,感兴趣的读者可以看「参考」篇章。

主要特点

  1. 效率高: 相较于 selectpollepoll 可以更高效地处理大量的并发连接。selectpoll 的效率随着监视的文件描述符数量增加而线性下降,而 epoll 则不会因为监视的文件描述符数量增加而显著降低效率。
  2. 扩展性好: epoll 使用一种称为事件通知的机制,只会处理那些真正发生了事件的文件描述符。这意味着系统不必重新检查所有文件描述符,从而大大减少了不必要的 CPU 开销。
  3. 支持边缘触发和水平触发: epoll 支持 Edge Triggered 和水平触发 Level Triggered 两种模式。边缘触发模式只在文件描述符状态改变时才通知应用程序,适用于非阻塞 I/O;而水平触发模式则在有事件可读或可写时都会通知应用程序,更容易使用但效率略低。

结论先行

epoll flow chart

工作原理

epoll 的工作可以分为三个主要步骤:

  1. 创建 epoll 实例: 使用 epoll_create 函数创建一个 epoll 实例。
  2. 添加/修改/删除文件描述符: 使用 epoll_ctl 函数将新的文件描述符添加到 epoll 实例中,或者修改、删除已存在的文件描述符。这些操作与文件描述符的数量无关,因此执行速度非常快。
  3. 等待事件发生: 使用 epoll_wait 函数等待事件的发生。这个函数可以同时监控多个文件描述符,当指定的文件描述符上发生了注册的事件时,函数返回,并告知哪些文件描述符上发生了事件。

ET & LT

epoll 中,边缘触发(ET, Edge Triggered)和水平触发(LT, Level Triggered)是两种不同的事件通知方式,它们定义了操作系统如何通知应用程序文件描述符上的 I/O 事件。

这两种模式的主要区别在于何时以及如何多次通知应用程序关于某个文件描述符的事件。

水平触发(Level Triggered)

  • 定义: 在水平触发模式下,只要文件描述符上有未处理的 I/O 事件存在,epoll_wait 就会通知应用程序。这意味着,如果数据可读取但未被完全读取,epoll_wait 会在下次调用时再次返回该文件描述符。
  • 行为: 这种模式更容易编程,因为应用程序可以不用担心在一个操作中处理所有数据。如果数据还在,epoll_wait 会继续通知你。
  • 适用场景: 更适合那些简单的应用或者对实时性要求不是非常高的应用,因为它简化了处理逻辑。

边缘触发(Edge Triggered)

  • 定义: 在边缘触发模式下,只有状态变化时(例如从无数据到有数据),epoll_wait 才会通知应用程序。一旦通知了应用程序某事件发生,除非有新的数据到达或状态再次发生变化,否则不会再次通知应用程序该事件。
  • 行为: 这要求应用程序必须立即处理所有事件,因为之后不会再收到关于这些事件的通知。这意味着应用程序必须循环读取或写入,直到数据被完全处理完,以确保不遗漏任何事件。
  • 适用场景: 适合需要高性能的场景,因为它减少了事件处理的次数,但要求程序必须更加小心地管理 I/O 操作。

比较和选择

  • 性能: 边缘触发通常提供更高的性能,因为它减少了系统调用的次数和不必要的事件处理。
  • 编程复杂性: 边缘触发模式编程比水平触发复杂,因为需要确保每次事件被彻底处理,并且更容易遇到如“惊群效应”(多个进程或线程被同一个事件唤醒)等问题。
  • 可靠性: 水平触发因为其简单的行为模式,在可靠性处理上更为直接和容易。

通常,选择哪种模式取决于应用的具体需求、预期的负载以及开发者对事件处理逻辑的控制程度。高性能服务器通常选择边缘触发模式,以最大化其效率,而简单的或者低负载应用可能会更倾向于使用水平触发,以简化开发和调试过程。

数据结构

epoll 使用 2 种关键的数据结构来维护和跟踪文件描述符(FD)和事件:

  1. 红黑树(Red-Black Tree): 用于存储所有注册的文件描述符及其事件。红黑树是一种自平衡二叉搜索树,能够在对数时间内完成插入、删除和查找操作,这使得管理大量文件描述符变得高效。
  2. 就绪列表(Ready List): 当事件发生(如可读、可写等)并被内核检测到时,相应的 FD 会被添加到一个就绪列表中。这个列表仅包含实际有事件发生的文件描述符,从而减少了 epoll_wait 调用的处理时间。

工作细节

epoll data structure
  1. 通过调用 epoll_create() 函数创建并初始化一个 eventpoll 对象。
  2. 通过调用 epoll_ctl() 函数把被监听的文件句柄 (如 socket 句柄) 封装成 epitem 对象并且添加到 eventpoll 对象的红黑树中进行管理。
  3. 通过调用 epoll_wait() 函数等待被监听的文件状态发生改变。
  4. 当被监听的文件状态发生改变时(如 socket 接收到数据),会把文件句柄对应 epitem 对象添加到 eventpoll 对象的就绪队列 rdllist 中。并且把就绪队列的文件列表复制到 epoll_wait() 函数的 events 参数中。
  5. 唤醒调用 epoll_wait() 函数被阻塞(睡眠)的进程。

事件监听

内核中的事件监听和回调机制是通过高效的事件驱动模型实现的,而不是简单的循环检查(如在用户空间中的轮询)。这种机制利用了现代操作系统的中断和回调系统,以及针对异步事件的优化处理策略。

以下是这个过程的详细解释:

1. 中断和中断处理

在硬件层面,大多数 I/O 操作(如网络通信、磁盘 I/O)都是通过中断驱动的。当一个 I/O 设备准备好数据或需要服务时,它会产生一个中断信号,这个信号被发送到 CPU。CPU 响应中断,并执行一个预定的中断处理程序(Interrupt Service Routine, ISR),该程序是由设备的驱动程序提供的。

2. 事件和回调

在 ISR 中,与设备相关的事件(例如网络包的接收、硬盘读取完成)会被检测到,并且可以在此阶段调用特定的回调函数。这些回调函数是在设备驱动或相关的内核模块中定义的,用来通知内核其他部分或者相关的进程有关事件的发生。

3. 文件描述符的回调机制

对于 epoll 等 I/O 多路复用技术,内核为每个文件描述符维护了一个事件处理机制。当文件描述符被创建时,相关的设备或资源会注册一组回调函数,这些函数会在特定的操作(如读、写、错误)上被触发。例如,一个网络套接字可能会在数据到达时触发一个“可读”事件的回调。

4. epoll 的事件绑定

当一个文件描述符被加入到 epoll 监听队列中,epoll 会利用这些回调来获得事件通知。epoll 操作相关的代码会将一个额外的回调函数绑定到这些文件描述符上。当文件描述符的状态改变时(如数据可读),这个回调函数将被触发,然后它会将相应的文件描述符标记为“就绪”,并放入 epoll 的就绪队列。

5. 事件通知和唤醒

epoll_wait 被调用且有事件就绪时,内核会检查就绪队列,并将这些事件传递给等待的进程。如果没有事件就绪,进程将被挂起直到有事件发生。事件的发生会触发内核调度程序唤醒相应的进程。

6. 效率和性能

这种基于中断的事件通知机制意味着内核不需要不断循环检查每个文件描述符的状态,从而极大地提高了效率。事件只有在实际发生时才被处理,且处理通常是由硬件中断直接触发的,这使得整个系统更加响应快速,减少了无效的 CPU 使用。

这种设计使得 Linux 内核在处理大量并发 I/O 操作时能够保持高效和稳定,适合构建高性能的网络服务和应用。

中断

中断机制是计算机硬件和操作系统核心功能之一,它允许外设或硬件异步地通知 CPU 需要处理某些事件。中断机制的实现并不依赖于类似于 for 循环的轮询检查,而是建立在更为直接和高效的硬件和处理器架构支持之上。

当 CPU 接收到中断信号时,它是通过一套内建于硬件的协调机制来识别和响应中断的。这个过程涉及硬件电路设计、处理器架构和操作系统的中断管理功能。

以下是 CPU 如何知道有中断发生,并且如何处理这一中断的详细步骤:

中断信号的检测和响应

  1. 中断请求线(IRQ):外部设备通过连接到处理器的一个特定的硬件线路(IRQ)发送中断信号。这个线路直接与处理器内的中断控制单元(Interrupt Controller)相连。

  2. 中断控制器:大多数现代计算机系统使用一个或多个中断控制器来管理中断信号。中断控制器的任务是接收来自各种外部设备的中断请求,并将这些请求优先级排序后发送给 CPU。

  3. 中断向量:当中断控制器接收到一个中断信号后,它会根据中断源确定一个中断向量。这个向量是一个数字,指向中断向量表中对应的入口,该入口包含了处理该中断的中断服务例程(ISR)的地址。

CPU 如何处理中断

  1. 当前指令的完成:当 CPU 接收到中断控制器发出的中断信号时,它首先会完成当前执行的指令。这是为了保证程序的状态能够正确保存,从而在中断处理完毕后可以无缝地恢复执行。

  2. 保存上下文:一旦当前指令执行完毕,CPU 会自动保存当前的程序状态,包括程序计数器(PC)、寄存器和其他必要的状态信息。这些信息通常被推送到当前的栈上。

  3. 跳转到 ISR:CPU 使用中断向量来访问中断向量表,找到与中断号对应的中断服务例程(ISR)的地址,并跳转到该地址开始执行 ISR。这个过程是自动的,由处理器的内部机制控制。

  4. 执行 ISR:中断服务例程会执行必要的操作来处理中断,比如读取数据缓冲区、清除设备状态或发送信号等。

  5. 恢复上下文并返回:一旦 ISR 执行完成,处理器会从栈上恢复之前保存的程序状态,并将控制权返回到被中断的程序,继续执行。

硬件支持

这一过程大量依赖于处理器的硬件支持,如中断向量表通常是固定在处理器的特定内存地址上的。此外,现代处理器如 x86 架构还提供了更高级的功能,比如支持多重中断控制器和高级可编程中断控制器(APIC)等。

这种基于硬件的中断响应机制允许 CPU 快速有效地处理各种外部事件,确保系统的响应性和稳定性。

参考