首页 > 数据库 >IO多路复用与Redis线程模型详解

IO多路复用与Redis线程模型详解

来源:互联网 2026-07-10 08:41:07

UnixIO模型分为阻塞、非阻塞、多路复用、信号驱动及异步五种。IO多路复用通过select、poll、epoll实现单线程监视多连接。Redis基于epoll实现Reactor模式,采用单线程文件事件处理器,通过队列有序处理套接字事件,避免多线程开销,高效处理并发连接。

Unix IO模型分类

在讨论IO模型之前,需要先理清几种常见形态。日常开发中频繁出现的IO模型主要包括五种:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO以及真正的异步IO。下面逐一解析。

阻塞IO - Blocking IO

阻塞IO是最传统的方式。用户线程发起IO请求后,内核会检查数据是否就绪——如果数据未就绪,线程会一直等待,同时交出CPU。直到数据准备完毕,内核将数据拷贝到用户线程,线程才恢复执行。

长期稳定更新的攒劲资源: >>>点此立即查看<<<

有人可能会考虑:是否可以通过多线程配合阻塞IO来提升效率?理论上可行,但代价是每个socket对应一个线程。在长连接场景下,线程资源持续占用,连接数增多时,性能瓶颈便会凸显。

非阻塞IO - NonBlocking IO

非阻塞IO的处理方式不同。用户线程发起操作后,不会等待,立即获得一个结果——如果返回错误码,说明数据尚未到达,后续可再次尝试。当内核数据准备好,用户线程再次请求时,数据会立即拷贝过来。整个过程中用户线程不交出CPU,但代价是需要不断轮询。

这里有一个关键对比:阻塞IO在数据未到时直接阻塞并放弃CPU;非阻塞IO则直接返回,CPU持续占用。然而,问题也很明显——while循环持续询问内核,CPU占用率会急剧升高,因此生产环境中很少采用纯轮询方式读取数据。

具体流程可以这样理解:用户进程发起recvfrom系统调用,内核发现数据未到,直接返回错误提示“稍后再试”。用户进程不确定数据何时到达,只能不断轮询。直到某次轮询时数据已就绪,内核将数据复制到用户缓冲区,返回成功标志,进程才开始读取。

IO多路复用 - IO multiplexing

非阻塞IO的轮询痛点催生了IO多路复用。这是一种同步IO模型,核心思想是单个线程同时监视多个文件句柄(即网络连接)。只要某个句柄就绪,就通知应用程序进行读写;若没有就绪,线程阻塞并交出CPU。这里的“多路”指多个网络连接,“复用”指同一个线程。

无论是阻塞还是非阻塞,单线程处理多个连接的能力都有限。而多路复用通过两个系统调用(select/poll/epoll + recvfrom)实现——注意阻塞IO仅使用recvfrom一个系统调用。select/poll/epoll的价值不在于更快,而在于能够同时处理多个connection。因此当连接数不高时,多路复用的性能未必优于多线程+阻塞IO。

在实现上,会有一个内核线程持续轮询多个socket的状态。只有当真正有读写事件发生时,才会调用实际的IO操作。由于只用一个线程管理多个socket,系统无需频繁创建进程或线程,也省去了维护开销,而且仅在真正读写时占用IO资源,从而大幅降低CPU占用率。

IO多路复用与Redis线程模型详解

信号驱动IO - signal driven IO

信号驱动模型采用另一种思路。用户线程发起IO请求时,先为对应的socket注册一个信号函数,然后继续执行其他任务。内核数据就绪后,发送一个信号给用户线程,用户线程在信号处理函数中发起实际的IO读写操作。不过该模型通常只适用于UDP——对TCP套接字几乎无用,因为信号产生过于频繁,且信号本身不指明具体事件。

流程大致如下:首先从用户空间开启套接字的信号IO功能,通过sigaction系统调用安装信号处理函数,然后直接返回。内核接收数据报后,向用户空间发送信号,信号处理函数收到后发起recvfrom,等待内核将数据复制到用户缓冲区,复制完成后返回成功,应用进程即可开始读取数据。

异步IO - asynchronous IO

前面四种都属于同步IO,只有最后一种才是真正的异步。因为多路复用和信号驱动在第二阶段(内核拷贝数据时)都会阻塞用户线程。而异步IO由POSIX规范定义,用户通知内核启动某个操作后,内核会在整个操作(包括等待数据和复制数据)全部完成之后,通知用户进程“数据已经准备好,可以读取了”。与信号驱动的区别在于:信号驱动是通知“可以启动一个IO操作”,而异步IO是通知“IO操作已经完成”。

同步与异步的定义

  • 同步:发起一个函数调用后,必须等待结果返回。结果要么是期望值,要么是异常。可理解为原子性操作——要么成功,要么失败返回。
  • 异步:发起调用后立即返回,无需等待结果。被调用者处理完毕后,通过回调、事件通知等方式“唤醒”调用方获取结果。
  • 小结:同步和异步关注的是程序之间的通信方式。

阻塞与非阻塞的定义

  • 阻塞:数据未到时持续等待,直到有数据才返回。
  • 非阻塞:数据未到时直接返回“无数据”,不等待。
  • 小结:阻塞和非阻塞关注的是程序等待结果时的状态。

因此,同步、异步与阻塞、非阻塞之间没有必然关联,它们关注的目标不同。

IO多路复用有哪些实现

主流的实现有三种:select、poll、epoll。

IO多路复用的大致实现

select是内核提供的一个多路分离函数,它解决了同步非阻塞IO中轮询等待的问题。

IO多路复用与Redis线程模型详解

用户首先将需要IO操作的socket添加到select中,然后阻塞等待select返回。当某个socket的数据到达时,select返回,用户线程再发起read请求读取数据。表面上看起来与阻塞IO差别不大,甚至多出了添加监视和调用select的步骤,效率似乎更差。但优势在于:用户可以在一个线程中同时处理多个socket的IO请求。只需注册多个socket,不断调用select来获取被激活的socket,即可实现单线程处理多个IO。而在同步阻塞模型中,要达到同样效果必须开启多线程。因此,IO多路复用的设计目标并非让单个请求更快,而是为了减少服务器因线程/进程数量过多而产生的压力。

当然,这种方式中每个IO请求的过程仍然是阻塞的(阻塞在select上),平均耗时可能比同步阻塞更长。如果能让用户线程只注册感兴趣的socket,然后执行其他任务,等到数据到来再处理,CPU利用率就能大幅提升。

IO多路复用与Redis线程模型详解

这就引出了Reactor模式。用户线程将轮询IO状态的工作交给handle_events事件循环。用户注册事件处理器后可以继续执行其他工作(异步),而Reactor线程负责调用内核的select检查socket状态。当有socket被激活时,通知对应的用户线程(或执行回调),处理数据。由于select本身是阻塞的,因此多路IO复用模型常被称为异步阻塞IO模型——这里的“阻塞”并非指socket阻塞,而是select调用阻塞。实际使用时,socket都设置为NONBLOCK,用户发起IO时数据已经到达,所以不会阻塞。

总的来说,IO多路复用是最常用的IO模型,但其异步程度并不彻底——因为select系统调用是阻塞的,所以只能称为异步阻塞IO,而非真正的异步IO。

select

select函数监视三类文件描述符:readfds、writefds、exceptfds。调用后会阻塞,直到有文件描述符就绪(可读、可写或有异常),或者超时。返回后通过遍历fdset来找到就绪的描述符。优点:跨平台性好。缺点:单个进程能监视的文件描述符数量有上限(Linux下默认1024),虽然可以修改宏或重新编译内核来扩大,但会降低效率;而且每次都要遍历整个fd集合,随着文件描述符增加,遍历时间线性增长。

poll

poll不再使用三个位图来表示fdset,而是采用一个pollfd结构体指针。pollfd结构中包含了要监视的event和实际发生的event,不再像select那样通过参数传值。而且pollfd没有最大数量限制(但数量过大时性能同样会下降)。返回后仍然需要轮询pollfd来获取就绪的描述符。优点:无需传递文件描述符集合,数量不再受限。缺点:依然要遍历所有描述符。

epoll

epoll是select和poll的增强版,更灵活,没有描述符数量限制。它使用一个事件表来管理多个描述符,将用户关心的文件描述符事件存储到内核的事件表中,从而用户空间和内核空间之间只需拷贝一次。用户请求时把事件注册到事件表,然后可以执行其他任务。内核有专门的线程处理事件列表,当某个文件事件准备就绪时,根据注册的事件信息通知对应的应用程序进行处理。这解决了select和poll在初始请求时的阻塞问题,但在数据获取阶段仍然是阻塞的。

Redis的线程模型

为什么 Redis 中要使用 I/O 多路复用

Redis运行在单线程上,所有操作顺序线性执行。但读写操作(等待用户输入或输出)是阻塞的,如果某个文件的IO阻塞了,整个进程就无法为其他客户端提供服务。为了让单线程的服务端同时处理多个客户端的事件,Redis引入了IO多路复用机制。

Redis线程模型实现

Redis基于Reactor模式封装了epoll,实现了一个网络事件处理器——文件事件处理器。它由四部分组成:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。由于文件事件分派器队列的消费是单线程的,因此Redis被称为单线程模型。

IO多路复用与Redis线程模型详解

消息处理流程

多个socket可能并发产生不同的操作,每个操作对应不同的文件事件。IO多路复用程序会监听所有socket,将产生事件的socket按照顺序、每次一个放入队列中排队——只有当一个事件执行结束后,才会放入另一个事件。事件分派器每次从队列中取出一个socket,根据事件类型交给对应的事件处理器处理。尽管多个文件事件可能并发出现,但I/O多路复用程序总是将所有产生事件的套接字推入一个队列,然后有序、同步、每次一个地传给分派器。上一个事件处理完毕,才会传送下一个。

I/O 多路复用程序的实现

Redis的I/O多路复用程序通过包装select、epoll、evport和kqueue这些函数库来实现。每个函数库在源码中对应一个单独的文件,例如ae_select.c、ae_epoll.c、ae_kqueue.c等。由于Redis为每个函数库实现了相同的API,因此底层实现可以互换,这在移植时非常方便。

文件事件的类型

I/O多路复用程序可以监听两种事件:AE_READABLE和AE_WRITABLE。对应关系如下:

  • 套接字可读时(客户端执行write或close操作),或者有新的可应答套接字出现时(客户端connect监听套接字),产生AE_READABLE事件。
  • 套接字可写时(客户端执行read操作),产生AE_WRITABLE事件。

如果同一个套接字同时产生了这两种事件,文件事件分派器会优先处理AE_READABLE,处理完后再处理AE_WRITABLE。也就是说,一个套接字既可读又可写时,服务器先读后写。

文件事件的处理器

Redis为文件事件编写了多个处理器,分别对应不同的网络通讯需求:

  • 连接应答处理器:用于对连接服务器的客户端进行应答。
  • 命令请求处理器:用于接收客户端发来的命令请求。
  • 命令回复处理器:用于向客户端返回命令执行结果。
连接应答处理器

本质上是对accept函数的包装。Redis服务器初始化时,将连接应答处理器和监听套接字的AE_READABLE事件关联起来。当客户端connect连接监听套接字时,触发AE_READABLE事件,执行连接应答处理器,完成应答操作。

命令请求处理器

对read函数的包装。客户端通过连接应答处理器成功连接后,服务器将客户端套接字的AE_READABLE事件和命令请求处理器关联。客户端发送命令请求时,触发AE_READABLE事件,命令请求处理器读取命令内容并传给相关程序执行。这个关联会持续整个连接过程。

命令回复处理器

对write函数的包装。当服务器有命令回复需要返回给客户端时,将客户端套接字的AE_WRITABLE事件和命令回复处理器关联。客户端准备好接收时,触发AE_WRITABLE事件,命令回复处理器将回复写入套接字。发送完毕后,解除关联。

一次完整的客户端与服务器连接事件示例

假设Redis服务器正在运行,监听套接字的AE_READABLE事件处于监听状态,对应处理者是连接应答处理器。此时一个客户端发起连接,监听套接字产生AE_READABLE事件,触发连接应答处理器执行——应答连接请求、创建客户端套接字和客户端状态,并将客户端套接字的AE_READABLE事件与命令请求处理器关联。

随后客户端发送命令请求,客户端套接字产生AE_READABLE事件,触发命令请求处理器,处理器读取命令内容并执行。执行后产生命令回复,服务器将客户端套接字的AE_WRITABLE事件与命令回复处理器关联。客户端尝试读取回复时,触发AE_WRITABLE事件,命令回复处理器将回复写入套接字,写入完成后解除关联。

IO多路复用与Redis线程模型详解

总结

IO模型的选择本质上是在资源占用、响应速度和开发复杂度之间进行权衡。从阻塞到非阻塞,从多路复用到真正的异步,每一步演进都针对特定的痛点。Redis通过单线程配合IO多路复用,巧妙地规避了多线程竞争和上下文切换的开销,同时又能高效处理大量并发连接,这一思路值得反复思考。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备2026025700号-3 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。