Skip to content

I/O 多路复用

I/O 多路复用是高性能服务端的核心技术,它允许单个线程同时监控多个文件描述符,任一描述符就绪时返回,从而实现"一个线程处理多个连接"。

select

select 是最早的多路复用系统调用,监听三类事件:可读、可写、异常。调用时传入三个 fd_set(文件描述符集合),内核修改 fd_set 返回就绪的描述符。

select 的限制

文件描述符数量限制:fd_set 使用固定大小的位图(1024 位),最多监听 1024 个描述符。性能随描述符数量线性下降:每次调用需要将 fd_set 从用户空间拷贝到内核空间,内核需要遍历所有描述符检查就绪状态。

select 的工作流程

应用程序调用 select,阻塞等待。内核遍历所有监听的描述符,检查是否有事件就绪。如果有事件就绪,修改 fd_set,返回就绪描述符数量。应用程序遍历 fd_set,处理就绪的描述符。

poll

poll 与 select 类似,但使用 pollfd 结构数组代替 fd_set。pollfd 包含:events(监听的事件)、revents(实际发生的事件)。没有 1024 的数量限制,但性能仍然随描述符数量线性下降。

poll 的优势

没有 1024 的数量限制,可以监听任意数量的描述符。pollfd 结构可以携带更多信息,如监听错误事件。

poll 的劣势

每次调用需要将 pollfd 数组从用户空间拷贝到内核空间,拷贝开销与描述符数量成正比。内核仍然需要遍历所有描述符检查就绪状态。

epoll

epoll 是 Linux 特有的多路复用机制,解决了 select 和 poll的性能问题。epoll 包含三个系统调用:epoll_create(创建 epoll 实例)、epoll_ctl(添加/删除/修改监听的描述符)、epoll_wait(等待事件)。

epoll 的核心思想

epoll 在内核中维护一棵红黑树,存储所有监听的描述符。描述符就绪时,内核将描述符添加到就绪链表。epoll_wait 只需要返回就绪链表中的描述符,时间复杂度 O(1)。

epoll_ctl

epoll_ctl 用于管理 epoll 实例监听的描述符。操作类型:EPOLL_CTL_ADD(添加描述符)、EPOLL_CTL_DEL(删除描述符)、EPOLL_CTL_MOD(修改监听事件)。事件类型:EPOLLIN(可读)、EPOLLOUT(可写)、EPOLLERR(错误)、EPOLLHUP(挂起)。

epoll_wait

epoll_wait 等待事件发生,返回就绪的描述符。参数:epoll 实例、events 数组(输出就绪事件)、maxevents(最多返回多少事件)、timeout(超时时间)。返回值:就绪描述符数量,0 表示超时,-1 表示错误。

epoll 的优势

不需要每次调用时传递所有监听的描述符,减少内核空间和用户空间的数据拷贝。内核中维护红黑树和就绪链表,时间复杂度 O(1),性能不随描述符数量下降。支持边缘触发模式,减少系统调用次数。

边缘触发 vs 水平触发

水平触发(Level-Triggered,LT)

默认模式。如果描述符就绪且未被处理,每次 epoll_wait 都会返回。特点:编程简单,不会丢失事件,但可能重复通知。适合阻塞和非阻塞 I/O。

边缘触发(Edge-Triggered,ET)

只有描述符状态从未就绪变为就绪时,epoll_wait 才返回。特点:减少系统调用次数,但编程复杂,必须一次性读完所有数据,否则数据会"丢失"。只适合非阻塞 I/O。

ET 的工作流程:描述符就绪时,必须循环读取直到返回 EAGAIN(表示没有数据了)。否则,剩余数据不会触发新的 epoll_wait 返回。这要求应用程序设置描述符为非阻塞模式。

epoll 的惊群效应

多个进程/线程监听同一个描述符,描述符就绪时,所有进程/线程都被唤醒,但只有一个能处理事件,其他被无效唤醒。这是内核的 bug,Linux 4.5+ 通过 EPOLLEXCLUSIVE 解决。

惊群效应的解决方案

使用 EPOLLEXCLUSIVE 标志,确保只有一个进程/线程被唤醒。使用 SO_REUSEPORT,每个进程/线程监听不同的套接字,内核根据四元组路由到特定套接字。使用主从 Reactor 模式,只有一个线程 accept,其他线程处理 I/O。

Reactor 模式

Reactor 模式是 I/O 多路复用的设计模式,将事件分发和处理分离。Reactor 单线程:一个线程监听事件,处理 I/O 和业务逻辑。Reactor 多线程:一个线程监听事件,多个线程池处理业务逻辑。主从 Reactor:主 Reactor 负责 accept,从 Reactor 负责 I/O 和业务逻辑。

Netty、Redis、Nginx 都采用 Reactor 模式的变体。理解 Reactor 模式,有助于理解高性能服务端的架构设计。