5种IO模型学习笔记 | 青训营
要了解5种IO模型,首先要明白IO操作会导致操作系统在用户态和内核态之间来回切换。
为了避免用户应用导致冲突甚至内核崩溃,用户应用与内核是分离的:
- 进程的寻址空间会划分为两部分:内核空间、用户空间
- 用户空间只能执行受限的命令,而且不能直接调用系统资源,必须通过内核提供的接口来访问
- 内核空间可以执行特权命令,调用一切系统资源
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
一、阻塞式IO( Blocking IO)
阻塞IO在两个阶段都会等待数据。
阶段一:
阶段二:
二、非阻塞式IO(New/No Blocking IO)
非阻塞IO操作会立即返回结果而不是阻塞用户进程。
阶段一:
阶段二:
非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
三、IO多路复用
无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom方法来获取数据,差别在于无数据时的处理方案:
- 如果调用recvfrom时,恰好没有数据,阻塞IO会使CPU阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。
- 如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据
而在单线程情况下,只能依次处理IO事件,如果正在处理的IO事件恰好未就绪(数据不可读或不可写),线程就会被阻塞,所有IO事件都必须等待,性能自然会很差。
IO多路复用是利用单个线程来同时监听多个IO事件,并在某个事件可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听IO事件的方式、通知的方式又有多种实现,常见的有:
- select
- poll
- epoll
四、信号驱动
- 信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有IO事件就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。
- 当有大量IO操作时,信号较多,SIGIO处理函数不能及时处理可能导致信号队列溢出,而且内核空间与用户空间的频繁信号交互性能也较低。
五、异步IO(Asynchronous IO)
异步IO的整个过程(两个阶段)都是非阻塞的,用户进程调用完异步API后就可以去做其它事情,内核等待数据就绪并拷贝到用户空间后才会递交信号,通知用户进程。
六、关于5种模型的对比
1、IO操作是同步还是异步,关键看数据在内核空间与用户空间的拷贝过程(数据读写的IO操作),也就是阶段二是同步还是异步:
2、一个简单的例子比较5种IO模型
假设你现在是个大厨(炖个老母鸡汤,切点土豆丝/姜丝/葱丝):
- 同步/阻塞:你站在锅边,一直等到汤炖好,期间不能做其他事情,直到汤炖好才去处理其他任务。
- 同步/非阻塞:你不断地查看锅里的汤,看是否炖好。在检查的间隙,你可以处理其他任务,如切菜。但你需要不断地切换任务,确保汤炖好了就可以处理。
- 异步/信号驱动:你给锅安装一个传感器,当汤炖好时,传感器会发出信号提醒你。在此期间,你可以处理其他任务,而不用担心错过汤炖好的时机。
- 异步 I/O:你请了一个助手,让他负责炖汤。当汤炖好时,助手会通知你。你可以专心处理其他任务,而无需关心炖汤的过程。
七、笔记总结
简单总结一下,IO 模型主要有五种:阻塞 I/O、非阻塞 I/O、多路复用、信号驱动和异步 I/O。
- 阻塞 I/O:应用程序执行 I/O 操作时,会一直等待数据传输完成,期间无法执行其他任务。
- 非阻塞 I/O:应用程序执行 I/O 操作时,如果数据未准备好,立即返回错误状态,不等待数据传输完成,可执行其他任务。
- 多路复用:允许一个线程同时管理多个 I/O 连接,适用于高并发、低延迟和高吞吐量场景,减少线程数量和上下文切换开销。
- 信号驱动:依赖信号通知应用程序 I/O 事件,适用于低并发、低延迟和低吞吐量场景,需要为每个 I/O 事件创建信号和信号处理函数。
- 异步 I/O:应用程序发起 I/O 操作后,内核负责数据传输过程,完成后通知应用程序。应用程序无需等待数据传输,可执行其他任务。
Redis在处理操作命令的线程中就使用的是IO多路复用模型,也就是使用单个线程去处理多个Socket连接,当操作命令就绪后,将IO事件转发给三个处理器去异步执行。Redis的单线程能够处理很高并发量请求与IO多路复用模型有着密切关系。