IO模型:同步、异步、阻塞、非阻塞
参考文章:
https://www.cnblogs.com/sheng-jie/p/how-much-you-know-about-io-models.html
https://songlee24.github.io/2016/07/19/explanation-of-5-IO-models/
在Linux的网络编程中,同步IO(synchronous IO)、异步IO(asynchronous IO)、阻塞IO(blocking IO)、非阻塞IO(non-blocking IO)究竟是什么?它们之间又有什么联系和区别? 何为同步异步?何为阻塞与非阻塞?二者的区别在哪里?阻塞在何处?为什么会有多种IO模型,分别用来解决问题?常用的框架采用的是何种I/O模型?各种IO模型的优劣势在哪里,适用于何种应用场景?
简而言之,对于I/O的认知,不能仅仅停留在字面上认识,了解内部玄机,才能深刻理解I/O,才能看清I/O相关问题的本质。
一、I/O 的定义
I/O 的全称是Input/Output。
计算机视角
I/O之于计算机,有两层意思:
1、I/O设备
2、对I/O设备的数据读写
对于一次I/O操作,必然涉及2个参与方,一个输入端,一个输出端,而又根据参与双方的设备类型,我们又可以分为磁盘I/O,网络I/O(一次网络的请求响应,网卡)等。
程序视角
I/O之于应用程序来说,强调的通过向内核发起系统调用完成对I/O的间接访问。换句话说应用程序发起的一次IO操作实际包含两个阶段:
1、IO调用阶段:应用程序进程向内核发起系统调用
2、IO执行阶段:内核执行IO操作并返回
准备数据阶段:内核等待I/O设备准备好数据
拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区
怎么理解准备数据阶段呢?
对于写请求:等待系统调用的完整请求数据,并写入内核缓冲区;
对于读请求:等待系统调用的完整请求数据;(若请求数据不存在于内核缓冲区)则将外围设备的数据读入到内核缓冲区。
应用程序进程在发起IO调用至内核执行IO返回之前,应用程序进程/线程所处状态,就是我们下面要讨论的几种IO模型。
二、I/O模型
在《UNIX网络编程.卷1》第6.2节介绍了五种IO模型,分别是:
- 阻塞式IO(blocking IO)
- 非阻塞式IO(non-blocking IO)
- IO复用(IO multiplexing)
- 信号驱动式IO(signal driven IO)
- 异步IO(asynchronous IO)
通常一个 socket 上的读操作包含两个阶段:
- 等待数据准备好;
- 将数据从内核拷贝到进程中。
上述几种IO模型就是在这两个阶段上各有不同的情况。
1、阻塞式IO
默认情况下,Linux下的所有socket都是阻塞的。以 UDP 的recvfrom
调用为例:
当进程调用recvfrom
时,该函数直到①数据报到达且被复制到应用进程缓冲区;②或者发生错误(比如被信号中断)才返回。
所以,阻塞式IO的特点就是在I/O执行的两个阶段都被阻塞了——阻塞等待数据,阻塞拷贝数据。
2、非阻塞式IO
Linux下可以通过fcntl
将 socket 设置为非阻塞模式。
当对一个非阻塞 socket 执行读操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个EWOULDBLOCK
错误;如果内核中有数据准备好了,它会立即将数据拷贝到用户内存,并成功返回。
由于非阻塞I/O在没有数据时会立即返回,故用户进程通常需要循环调用recvfrom
,不断地主动询问内核数据是否ready。
所以,非阻塞式IO的特点是在I/O执行的第一个阶段不会阻塞线程,但在第二阶段会阻塞。
3、IO复用
IO复用(IO multiplexing),也称事件驱动IO(event-driven IO),就是在单个线程里同时监控多个套接字,通过 select 或 poll 轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
可以看出,进程阻塞在select
调用上,等待有套接字变为可读;当有套接字可读以后,调用recvfrom
把数据报从内核复制到用户进程缓冲区,此时进程阻塞在IO执行的第二个阶段。
如上图整个用户进程其实是一直被阻塞的,但IO复用的优势在于可以等待多个描述符就绪。
所以,IO复用的特点是进行了两次系统调用,进程先是阻塞在 select/poll 上,再是阻塞在读操作的第二个阶段上。
4、信号驱动式IO
信号驱动式IO(signal-driven IO),就是让内核在描述符就绪时发送SIGIO
信号通知用户进程。
首先需要开启 socket 的信号驱动式IO功能,然后通过sigaction
系统调用注册SIGIO信号处理函数 —— 该系统调用会立即返回。当数据准备好时,内核会为该进程产生一个SIGIO信号,这时就可以在信号处理函数中调用 recvfrom 读取数据了。
所以,信号驱动式IO的特点就是在等待数据ready期间进程不被阻塞,当收到信号通知时再阻塞并拷贝数据。
5、异步IO
异步IO(asynchronous IO)其实用得很少,在Linux 2.5 版本的内核中首次出现,在 2.6 版本的内核中才成为标准特性。
用户进程在发起aio_read
操作后,该系统调用立即返回 —— 然后内核会自己等待数据ready,并自动将数据拷贝到用户内存。整个过程完成以后,内核会给用户进程发送一个信号,通知IO操作已完成。
异步IO与信号驱动式IO的主要区别是:信号驱动式IO是由内核通知我们何时启动一个IO操作,而异步IO是由内核通知我们IO操作何时完成。
所以,异步IO的特点是IO执行的两个阶段都由内核去完成,用户进程无需干预,也不会被阻塞。
6、五种IO模型的比较
可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:都是阻塞于recvfrom
调用,将数据从内核拷贝到用户进程缓冲区。
之所以称为异步IO,取决于IO执行的第二阶段是否阻塞。因此前面讲的BIO,NIO和SIGIO均为同步IO。
三、阻塞vs非阻塞,同步vs异步
回到本文开头的那个问题:同步IO、异步IO、阻塞IO、非阻塞IO究竟是什么?它们之间又有什么联系和区别?
阻塞IO vs 非阻塞IO
上面介绍阻塞式IO模型、非阻塞式IO模型时已经说明了两者的区别:
- 阻塞I/O会一直阻塞用户进程直到操作完成
- 非阻塞I/O在内核的数据还没准备好的情况下会立即返回
同步IO vs 异步IO
POSIX是这样定义的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes. —— 同步IO操作导致进程阻塞,直到IO操作完成。
- An asynchronous I/O operation does not cause the requesting process to be blocked. —— 异步IO操作不导致进程阻塞。
上面定义中的I/O operation
是指真正的I/O系统调用,比如recvfrom
,所以阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型、信号驱动式I/O模型都属于同步I/O。—— 只有异步I/O模型属于POSIX定义的异步I/O,因为在异步I/O模型中,用户进程是将整个I/O操作都交给内核来完成,内核完成后发信号通知,在此期间用户进程完全不用去理会。