掘金 后端 ( ) • 2024-04-24 10:15

本文从什么是IO多路复用入手,用最简单的方式介绍什么是IO多路复用,之后对epoll poll 与 select 三种主要的IO多路复用技术进行介绍。

1.什么是IO多路复用

IO多路复用是在网络IO的过程中复用系统调用和处理器(CPU)资源。 以一次HTTP的HTML请求为例,在传统的阻塞式I/O模型中,每个连接或文件描述符都需要一个独立的系统调用来等待其数据就绪:

image.png

使用I/O多路复用的应用程序只需一个或少数几个线程(而不是与连接数相等的线程)就可以处理所有连接的I/O事件:

image.png

使用I/O多路复用,进程可以在等待多个连接的I/O事件时处于阻塞状态,一旦某个连接的数据准备好,进程被唤醒并仅处理已就绪的连接,然后再次进入阻塞等待状态。这种模式下,处理器资源得到了更有效的利用,因为不需要为每个连接维持一个独立的运行线程,而且减少了因线程调度带来的额外负担。

2.select、poll 与 epoll

epoll, poll, 与 select 是 Linux 系统中用于实现 I/O 多路复用 的三种主要技术。它们均允许单个进程或线程同时监控多个文件描述符(如套接字)的 I/O 状态,从而高效地处理大量的并发连接,而无需为每个连接创建独立的线程或进程。以下是它们之间的主要区别:

  1. select

数据结构限制:使用固定大小的 fd_set 结构体来存储待监控的文件描述符集合,这意味着可监控的文件描述符数量存在硬性上限(通常为 FD_SETSIZE,典型值为 1024)。当需要监控的描述符数量超过这个限制时,需要重新定义该常量并重新编译内核,或者分批处理。

效率问题:每次调用 select 时,都需要将用户空间的 fd_set 数据结构复制到内核空间,这在描述符数量较大时会带来额外的开销。另外,select 在检查文件描述符状态时采用轮询方式,即逐个检查每个描述符是否就绪,其复杂度与待监控描述符的数量成正比,导致在高并发场景下的性能下降。

无事件通知机制:当某个描述符就绪时,select 不会主动通知调用者,而是需要进程不断轮询(即重复调用 select),增加了不必要的系统调用开销。 2. poll

改进的数据结构:poll 使用 pollfd 结构体数组代替了 select 的 fd_set,消除了描述符数量的固定限制,理论上可监控任意数量的文件描述符。不过,poll 仍然需要在每次调用时传递完整的数组到内核,如果描述符数量很大,这仍可能导致较大的复制开销。

同样采用轮询:poll 同样采用轮询方式检查文件描述符状态,其性能瓶颈与 select 类似,即随着待监控描述符数量的增长,检查效率会降低。

无事件通知机制:poll 也没有提供事件通知机制,进程仍需不断轮询以检测新的就绪事件。

  1. epoll

高效数据结构:epoll 使用了一种更加高效的数据结构(通常是红黑树)来存储待监控的文件描述符,并且仅当描述符状态改变时才会通知用户空间。这样避免了每次调用时的大规模数据复制,尤其是对于大量描述符的情况。

事件驱动:epoll 提供了事件驱动的机制。当调用 epoll_wait() 时,内核只返回已经就绪的描述符,而不是像 select 和 poll 那样遍历所有描述符。此外,epoll 支持两种工作模式: ● LT (Level Triggered):水平触发,只要描述符处于就绪状态,每次调用都会返回。 ● ET (Edge Triggered):边缘触发,仅当描述符状态从非就绪变为就绪时返回一次,直到下次状态变化。

事件通知:epoll 通过内核事件队列实现高效的事件通知。当任何被监控的描述符状态发生变化时,内核会自动将就绪事件放入队列中等待进程检索,减少了不必要的系统调用。

综上所述,epoll 在处理大量并发连接时具有显著优势,尤其是在高并发、大规模 I/O 操作的场景下,其无上限的描述符支持、低开销的事件通知机制以及高效的事件处理方式使其成为首选。相比之下,select 和 poll 虽然功能相似,但在面对大量描述符或高并发需求时,由于其数据结构限制、轮询机制以及缺乏事件通知的特性,可能会导致性能瓶颈和资源浪费。然而,对于描述符数量较少、对性能要求不高的应用,select 和 poll 仍能满足基本需求,并且它们的实现和使用相对简单。