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 上的读操作包含两个阶段:

  1. 等待数据准备好;
  2. 将数据从内核拷贝到进程中。

上述几种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操作都交给内核来完成,内核完成后发信号通知,在此期间用户进程完全不用去理会。

 

 



 

posted on 2020-08-14 14:38  麦克煎蛋  阅读(134)  评论(0编辑  收藏  举报