简述同步 IO、异步 IO、阻塞 IO、非阻塞 IO 之间的联系与区别
POSIX
同步 IO、异步 IO、阻塞 IO、非阻塞 IO,这几个词常见于各种各样的与网络相关的文章之中,往往不同上下文中它们的意思是不一样的,以致于我在很长一段时间对此感到困惑,所以想写一篇文章整理一下。
POSIX(可移植操作系统接口)把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO
按 POSIX 的描述似乎把同步和阻塞划等号,异步和非阻塞划等号,但是为什么有的人说同步 IO 不等于阻塞 IO 呢?先来说说几种常见的 IO 模型吧。
IO 模型
这里统一使用 Linux 下的系统调用 recv 作为例子,它用于从套接字上接收一个消息,因为是一个系统调用,所以调用时会从用户进程空间切换到内核空间运行一段时间再切换回来。默认情况下 recv 会等到网络数据到达并且复制到用户进程空间或者发生错误时返回,而第 4 个参数 flags 可以让它马上返回。
- 阻塞 IO 模型
使用 recv 的默认参数一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞。A 同学用杯子装水,打开水龙头装满水然后离开。这一过程就可以看成是使用了阻塞 IO 模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种 IO 模型是同步的。
- 非阻塞 IO 模型
改变 flags,让 recv 不管有没有获取到数据都返回,如果没有数据那么一段时间后再调用 recv 看看,如此循环。B 同学也用杯子装水,打开水龙头后发现没有水,它离开了,过一会他又拿着杯子来看看…… 在中间离开的这些时间里,B 同学离开了装水现场 (回到用户进程空间),可以做他自己的事情。这就是非阻塞 IO 模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间 (等着水将水杯装满),因此它还是同步 IO。
- IO 复用模型
这里在调用 recv 前先调用 select 或者 poll,这 2 个系统调用都可以在内核准备好数据 (网络数据到达内核) 时告知用户进程,这个时候再调用 recv 一定是有数据的。因此这一过程中它是阻塞于 select 或 poll,而没有阻塞于 recv,有人将非阻塞 IO 定义成在读写操作时没有阻塞于系统调用的 IO 操作(不包括数据从内核复制到用户空间时的阻塞,因为这相对于网络 IO 来说确实很短暂),如果按这样理解,这种 IO 模型也能称之为非阻塞 IO 模型,但是按 POSIX 来看,它也是同步 IO,那么也和楼上一样称之为同步非阻塞 IO 吧。
这种 IO 模型比较特别,分个段。因为它能同时监听多个文件描述符 (fd)。这个时候 C 同学来装水,发现有一排水龙头,舍管阿姨告诉他这些水龙头都还没有水,等有水了告诉他。于是等啊等 (select 调用中),过了一会阿姨告诉他有水了,但不知道是哪个水龙头有水,自己看吧。于是 C 同学一个个打开,往杯子里装水 (recv)。这里再顺便说说鼎鼎大名的 epoll(高性能的代名词啊),epoll 也属于 IO 复用模型,主要区别在于舍管阿姨会告诉 C 同学哪几个水龙头有水了,不需要一个个打开看 (当然还有其它区别)。
- 信号驱动 IO 模型
通过调用 sigaction 注册信号函数,等内核数据准备好的时候系统中断当前程序,执行信号函数 (在这里面调用 recv)。D 同学让舍管阿姨等有水的时候通知他 (注册信号函数),没多久 D 同学得知有水了,跑去装水。是不是很像异步 IO?很遗憾,它还是同步 IO(省不了装水的时间啊)。
- 异步 IO 模型
调用 aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。E 同学让舍管阿姨将杯子装满水后通知他。整个过程 E 同学都可以做别的事情 (没有 recv),这才是真正的异步 IO。
最后,总结比较下五种 IO 模型:
总结
IO 分两阶段:
1.数据准备阶段
2.内核空间复制回用户进程缓冲区阶段
一般来讲:阻塞 IO 模型、非阻塞 IO 模型、IO 复用模型 (select/poll/epoll)、信号驱动 IO 模型都属于同步 IO,因为阶段 2 是阻塞的 (尽管时间很短)。只有异步 IO 模型是符合 POSIX 异步 IO 操作含义的,不管在阶段 1 还是阶段 2 都可以干别的事。
- ps:以上图片均截自 UNIX 网络编程卷 1。