五种IO模型
在网络通信中,进程需要频繁的与网络进行数据交互,这个过程会涉及到非常复杂的问题。
应用进程运行在操作系统之上,想要收发网络数据,必须调用操作系统提供的系统接口,由操作系统和互联网交互,真正完成网络通信。
整个流程里,进程不会直接和网络打交道,仅与操作系统交互,依靠发送缓冲区、接收缓冲区这两块内核内存完成数据流转。
缓冲区本质就是一段内存空间,用户进程与操作系统内核均可读写这片内存,以此传递数据:
- 当操作系统从网络收到数据,会先把数据拷贝到接收缓冲区;之后用户进程调用接口(Linux TCP通信常用
recvfrom),就能从接收缓冲区读取数据。 - 当用户进程需要向外发送数据,先将数据拷贝至发送缓冲区;操作系统内核再读取缓冲区里的数据,发送到网络中。
站在应用进程视角,这一系列数据读写动作就是输入(Input)与输出(Output),简称IO。
简言之:网络通信的本质,是进程和操作系统之间持续进行大批量数据IO拷贝。
但网络环境十分复杂:数据到达速度时快时慢,高速涌入时缓冲区容易溢出;低速时缓冲区又会长时间没有新数据;TCP协议还存在半包、粘包等现象。种种问题,使得网络IO面临的挑战远大于普通文件IO。
针对复杂的网络场景,如何高效实现进程与缓冲区之间的数据拷贝,衍生出了经典的五大IO模型。
同步IO
一个网络通信的IO过程,可以划分为两步:等待数据就绪、拷贝数据。IO模型讨论的重点,是如何等待数据,因为等待数据需要占用很多时间,而且它带有不可预期的性质,没人知道下一个数据到达是什么时候。
这个过程可以简单理解为取外卖,当你在宿舍楼点了一份外卖,你无法预期外卖什么时候到,那么此时你就有不同的方式去等待这份外卖。
后续会多次使用外卖这个例子,这里简单说明一下取外卖与IO行为的对应关系:
- 等待数据:在宿舍楼楼下等待外卖
- 拷贝数据:把拿到的外卖,提到宿舍楼上
- 处理数据:在宿舍吃外卖
阻塞IO
- 阻塞IO:在数据准备好之前,进程一直等待,直到有数据到达
阻塞IO是所有套接字的默认方式。
这个过程可以理解为,当你点了一份外卖后,你就一直在宿舍楼门口等待外卖小哥,从下单开始,只要外卖小哥不来,你就一直在宿舍楼下等。
简单解释一下这张图片,我把进程的状态分为了三种,分别是红、黄、绿三种颜色。
- 等待数据:这个时候是进程最低效的时候,因为它除了等待啥也不干,几乎可以说是在浪费计算机的资源,占用了系统的时间,还不处理数据
- 拷贝数据:此时数据已经就绪,进程正在把数据从缓冲区拷贝出来
- 处理数据:此时进程已经拿到了数据,正在处理数据,这是进程最高效的时候,因为它拿着计算机的资源,去做了真正有意义的数据处理工作
当进程调用完网络通信的系统调用后,进入了一大段时间的等待,直到内核接收到数据,此时进程才把数据拷贝出来,最后处理数据。
可以看出,这其实是一种非常低效的方式,进程花费了大量的时候在等待数据上。
非阻塞IO
- 非阻塞IO:如果数据还没有准备好,系统调用直接返回EWOULDBLOCK错误码,不再等待数据
这种行为可以理解为:当点完外卖后,你就去宿舍楼下看看外卖到了没有,如果没到,就回到宿舍楼上干别的事,过一会再下去看看到了没有,以此类推,直到某一次看到外卖到达,此时就可以把外卖提到楼上(数据拷贝),最后吃饭(处理数据)了。
如图所示:
当调用系统调用后,系统调用会检查数据是否就绪,如果没有就绪,那么返回EWOULDBLOCK错误码,表示没有数据可以读取。当进程知道数据没有到达,那么可以先去做别的事情,如此往复。直到某一次执行系统调用,发现数据就绪,那么进程开始拷贝数据,最后处理数据。
在图片中,要解释三个注意点:
- 在进程调用系统调用和返回
EWOULDBLOCK期间,进程进入了短暂的红色等待数据状态,这是因为调用函数,检查数据是否就绪,这是有成本的。就像取外卖的过程中,一直上下楼是需要消耗体力的。 - 当进程接收到
EWOULDBLOCK,进程会进入一段时间的绿色处理数据状态,这个时候处理的数据不一定是网络数据,也可以是其他数据,只要进程在真真正正的处理业务,那么计算机的资源就不算浪费。就像取外卖过程中,你虽然没有取到外卖,但是回到宿舍后,你依然可以去写一个小题目,开一局小游戏,这样都不算浪费时间。 - 系统调用本身就是用来读取数据的,阻塞IO与非阻塞IO,调用的函数是完全一样的,只是函数的行为不同。所以最后一次进程调用了相同的系统调用,它直接进入了拷贝数据的状态,因为这个函数最初的功能,就是拷贝数据。只是说,在阻塞IO中只要函数返回,那么数据一定准备好了。但是在非阻塞IO中,函数返回后数据也不一定准备好了,还需要检查一下错误码
EWOULDBLOCK来判断。
非阻塞IO虽然看起来比阻塞IO处理数据的时间更多,也就是CPU处理真正有用数据的占比更高。但是其实反复调用系统调用,检查数据是否就绪,是要消耗大量CPU资源的,所以其实很少用这种非阻塞IO,这个反复检查数据是否就绪的过程,称为轮询。
就好像在取外卖的过程中,你需要反复上下楼梯很多次,虽然说每次你回到宿舍,可以把时间利用起来做其他事情,但是这对你的体力是很大的消耗。
信号驱动IO
- 信号驱动IO:内核把数据准备好后,发送信号SIGIO通知进程
这一模式和外卖软件的场景十分贴合:下单后,你无需守在楼下等待,也不用反复下楼查看。只需等待手机送达通知;收到提醒后,再下楼提取外卖(拷贝数据),最后回宿舍用餐(处理数据)。
如图所示:
当进程开始通信后,进程调用一个 sigaction 的系统调用,这个函数可以指定信号的处理方式,此处指定的是 SIGIO 信号的处理方式。
当指定完信号的处理方式后,进程就可以去做自己的事情了,进入了长时间的绿色处理数据状态,只不过处理的是其它的数据,不是网络数据。
直到内核把数据准备完毕,给进程发送了一个 SIGIO 信号,此时进程就去执行先前通过 sigaction 预设的函数,把数据拷贝出来,最后处理数据。
可以看出,这种方式下,进程可以省去大量的等待数据所消耗的时间,把精力放在处理数据上面。
多路转接IO
- 多路转接IO:同时等待多个网络数据,只要任意一个就绪,就返回
这种行为像是一个宿舍内部有多个人点了外卖,最后决定让同一个人去取,那么这个人就去宿舍楼下一直等,只要有任何一个人的外卖到了,就提到宿舍楼上。随后再下去等待下一个外卖。
如图所示:
多路转接存在多种实现方案,这里以系统调用 select 为例进行说明。select 能够同时监听多个网络套接字;只要任意一个套接字的数据就绪,select 就会返回,随后程序便可执行数据拷贝与数据处理流程。
⚠️ 重点区分:select 仅负责检测数据是否就绪,本身不参与数据拷贝,它只起到就绪通知的作用。检测到就绪后,仍然需要调用专门的数据读取系统调用获取数据。
select 拥有多种使用模式,既可以在单进程内监听多个套接字,也支持多进程协作,典型场景如下:
- 单进程场景
一个进程维护大量套接字,借助select同时对所有套接字进行就绪检测。一旦某个套接字数据就绪,进程立刻执行数据拷贝、业务处理,之后再次调用select等待下一次数据到达。 - 多进程场景
多个进程各自管理套接字,可将就绪检测任务统一交给单独一个进程。该进程仅调用select监控数据状态;当数据就绪时,通知其他进程完成数据拷贝与业务处理。
这套思路同样可以延伸至多线程模型,实现方式较多,此处不再展开。
如果仔细观察,可以发现多路转接IO和阻塞IO非常非常像,如下图所示:
左侧为多路转接IO,右侧为阻塞IO,二者表面相似,但存在两处核心区别:
-
系统调用拆分不同
阻塞IO全程只调用一个系统调用,该调用同时完成「等待数据」与「拷贝数据」两个阶段。
多路转接IO将两个阶段拆分:由select负责等待数据;当检测到数据就绪后,还需要额外调用一套独立系统调用,完成数据拷贝。
这也是多路转接IO流程多出两步操作的原因:select调用返回、再次发起读取调用拷贝数据。 -
监听能力不同
多路转接IO的有效等待耗时更短。select能够同时监听多个套接字;而阻塞IO同一时刻只能等待单个套接字。监听的套接字数量越多,平均等待到数据的间隔就越短。
沿用外卖例子辅助理解:
假设宿舍4个人分别下单外卖,一份外卖平均等待30分钟,每隔5分钟就有一人下单。
阻塞IO模式:4个人下单后各自守在楼下,每个人只等待自己的外卖,总体等待时长累加,合计等待2小时。

虽然最后总时长只用了45min,但是其中存在大量的重叠时间,这样就同时在浪费多个人的时间,所以实际浪费的时间是2h。
而多路转接IO就是四个人中,只派出一个同时等待四个人的外卖,等到了一个外卖,就拿一份外卖。
当让一个人同时等待四份外卖后,那么其他人点完外卖就可以处理自己的数据了,最后其实只有一个人在等待,实际等待的时间就是分钟。
可以看出多路转接IO在面对多个套接字情况下,是非常高效的。因为它避免了多个进程同时等待,节省了这些重叠的时间。当多路转接同时处理的套接字越多,那么它的优势就越明显,面对多个套接字的情况下,它是所有同步IO中最高效的一种。
异步IO
- 异步IO:由内核完成等待数据和拷贝数据的过程,最终进程可以直接处理数据
这种模式类似于送上门的外卖,当你下单了一份外卖,外卖直接送到你宿舍内部,不再需要你亲自下楼去拿。
如图所示:
当进程准备接收网络数据时,调用 aio_read 直接把任务交给Linux内核,随后内核就会完成所有工作,比如等待数据,拷贝数据等等。最后把数据放在进程指定的位置,发送一个信号给进程,告知数据到达,进程就可以直接处理数据了。
而在进程告知内核去读取网络数据后,进程自己完全处于空闲状态,可以去处理其它任务,这就是异步IO。
异步IO和信号驱动IO有点像,都是发送信号告知进程数据到达,但是两者区别其实很大:
左侧是异步IO,右侧是信号驱动IO。可以发现,异步IO的信号发送后,数据已经可以直接处理了,但是信号驱动IO下,只是告知进程数据已经到达内核了,但是数据还需要由进程自己进行拷贝。
这也就是IO模型中,异步与同步最本质的区别:
- 异步:由内核完成数据拷贝,进程可以直接处理数据
- 同步:由进程自己把内核中的数据拷贝出来
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)