再谈阻塞与非阻塞,同步与异步

前言

作为程序猿经常会听到阻塞非阻塞,同步异步的概念,前辈也经常告诫我们这些概念是经常会使用到的,面试经常会问。网上的经典解释是以一个烧开水的例子说明的,内容如下:

老张烧水,水壶放到炉子上,然后专心等待水烧开——同步阻塞
老张烧水,水壶放到炉子上,然后去客厅看电视,时不时去看看水有没有烧开——同步非阻塞
老张烧水,使用响水壶,水放到炉子上后等待水壶响——异步阻塞
老张烧水,使用响水壶,水放到炉子上后就去客厅看电视,等待水壶响后提壶——异步非阻塞

但我觉得解释的十分别扭,因此自己做了些研究并有了些心得,在这里与大家分享一下。

概要

本文首先提出这四个词是形容词,理解必须包含上下文。它们多用来形容网络I/O模型。然后对linux 5种网络I/O模型进行了介绍从中抽象概念。作者认为阻塞和非阻塞主要是形容流程的,同步异步是形容交互模式的。请求方的流程因为数据请求而被阻塞则这种交互模式是同步的,反之没有因为数据请求而阻塞流程的交互模式是异步的。最后作者阐述了对使用烧开水来解释这四个词的看法。

定题

首先我们来确定问题。如果一个面试官直接让你解释同步与异步,阻塞与非阻塞这几个概念,那我可以说这个面试官问法是相当有问题的。因为同步,异步,阻塞,非阻塞是形容词,不是名词。我们只有将这几个形容词放在相应的上下文或者确定其描述的主体在来解释才有意义。那么同步,异步,阻塞,非阻塞是描述什么的呢?

根据我的了解其实这些词在大部分语境描述的是网络IO模型。下面我们来解释下Linux的IO模型,来看看在描述IO模型时这四个形容词到底指的是什么。

Linux 网络I/O模型

概念

为了说清楚Linux I/O模型,我们需要先搞清楚一些概念。

首先什么是操作系统?我认为操作系统是向下屏蔽硬件资源的差异,向上为用户提供统一资源服务。这些硬件资源主要包括磁盘资源,外设资源,内存资源和CPU资源。操作系统将外设和磁盘资源抽象成了文件,将内存资源抽象成了地址空间,将CPU资源抽象成了进程或线程,方便管理及使用。针对这些抽象操作系统提供了相应服务贡用户程序使用,比如CPU调度服务,内存地址服务以及针对设备的IO服务。
操作系统抽象

Linux操作系统的用户空间内核空间。操作系统在提供内存服务时,为了保护操作系统使用的内存绝对不会被用户程序破坏。将操作系统内核使用的内存空间叫作内核空间,将其它应用使用的内存空间叫作用户空间。

进程状态,进程在CPU上运行时,主要会出现3种状态(细分还有5状态,7状态),分别是就绪,运行,阻塞。当进程资源准备齐全等待CPU时间片时进程进入就绪状态,当获取CPU时间后进入运行状态,运行时间消耗完再次进入就绪状态,在运行时缺少资源(网络数据)则进入阻塞状态,在阻塞时获取到全部资源则进入就绪状态。

缓存IO,缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O,即基于缓存进行I/O操作。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中(内核空间),然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间(用户空间)。

网络I/O模型

在大致了解了以上概念后,我们开说说Linux为我们提供的I/O模式,即I/O的操作方式。首先再次强调,操作系统在向用户程序提供I/O服务时大体需要分为2个步骤,从磁盘或网卡等硬件中读取数据到内核空间的缓冲区中,或称成为等待数据就绪。从内核空间的数据拷贝到用户空间供程序使用。

由于分为两个阶段以及网络I/O的特殊性,linux为用户程序提供了5种网络I/O处理的方案:

  • 阻塞 I/O(blocking IO)
  • 非阻塞 I/O(nonblocking IO)
  • I/O 多路复用(IO multiplexing)
  • 信号驱动 I/O(signal driven IO)
  • 异步 I/O(asynchronous IO)

阻塞 I/O(blocking IO)

在linux中,最简单常用的网络I/O模型是所有socket都blocking的阻塞I/O,根据经典教程unix网络的描述,它的流程如下图:
BIO

当用户向内核发起了请求获取的数据的系统调用后,CPU会从用户态切换到内核态。当kernel一直等到数据准备好了即数据已从网卡写入到内核缓冲区,接着CPU就会将数据从内核空间中拷贝到用户内存,然后kernel返回结果,用户进程才解除阻塞状态,重新运行起来。 所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞I/O(nonblocking IO)

linux下,可以将secket通信设置为non-blocking。其流程如下图所示:
nio

该模式下用户程序在使用系统调用向内核请求数据时,会告诉内核如果数据没准备好返回ERROR而不是让用户程序阻塞。从用户程序角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户程序判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,在nonblocking IO中用户程序需要不断的主动询问kernel数据好了没有。这通常会浪费CPU时间,但是通常在专用于一个功能的系统上偶尔会遇到此模型。

多路复用(IO multiplexing)

多路复用IO是一个非常重要的IO模型,这种IO模型在有些地方被称为event driven IO。在这种模式下又分为3种机制,select\poll\epoll,相同的是它们都会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。流程图如下:
mio

在这种模式下用户程序会用select这个系统调用开始监视所有负责的socket,如果任何数据准备好select会终止阻塞并返回。然后程序再用另一个系统调用将数据从内核空间读取到用户空间。这个模型精妙之处在于用2个进程分别处理2类阻塞。监控CPU是否将数据准备好的进程同时处理多个链接,而拷贝数据则交给另一类进程。这种模式的优点是可以处理多个connection。如果处理的链接数不是很高的话,这种模式不一定定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大(使用了2种2次系统调用,blocking只用1次阻塞1次)。在是实际的IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

信号驱动 I/O(signal driven IO)

在信号驱动I/O的模式下用户程序告诉内核,当内核数据准备好后使用SIGIO信号通知用户程序。具体流程如下
signalIO

在这种模式下用户程序需要先准备好SignalDriven I/O的socket,使用sigaction系统调用。这时内核会立马返回,用户程序可以继续运行。此处它是非阻塞的。当报文已经保存到内核缓冲区后,内核会返回针对我们程序的SIGIO信号,然后用户程序就可以将数据从内核空间读到用户空间了。这种方式的最大好处就是在等待数据准备完成时不会阻塞用户程序。

异步 I/O(asynchronous IO)

异步I/O是POSIX定义的规范。这种模式与信号驱动类似,不同的是内核会在数据已经完全进入用户空间后再通知用户程序,而不是数据准备好就通知用户程序。流程如下图
aio

用户程序使用aio_read开始异步I/O,同时还需要告诉内核descriptor,buffer指针,buffer大小(read也是这3个变量),file offset以及如何通知用户程序传输完成。该调用立刻返回,不会阻塞去等待IO完成。在上图的例子中展示的操作系统使用信号量的方式通知用户程序数据已读去完成。

Synchronous I/O VS Asynchronous I/O

在谈对同步异步,阻塞非阻塞的看法前,我们先看下同步和异步IO的定义,以下是POSIX——可移植操作系统接口(Portable Operating System Interface of UNIX )的定义

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  • An asynchronous I/O operation does not cause the requesting process to be blocked.

意思就是同步IO操作会导致请求程序阻塞而异步IO操作会导致。运用这个定义blocking, nonblocking, I/O multiplexing, 和signal-driven I/O都是同步IO操作,因为实际的IO操作(recvfrom)会阻塞用户程序。只有asynchronous IO是异步IO。这句话不是我说的是<<unix网络编程 v1.3>>这本书6.2小节的最后一句话,不信自己去查。

我的见解

说了这么多下面说说我关于同步异步,阻塞非阻塞我的见解。

  1. 从网络I/O模型分类上看有阻塞I/O、非阻塞I/O、异步I/O,没有同步I/O这个名词。
  2. 如果将I/O模型用同步和异步来分类的话,那么除了异步I/O剩下的都是同步I/O。
  3. 判断同步异步I/O模型的依据是请求程序是否被内核阻塞
  4. 在描述网络I/O模型的时候四个词的意思大体如此,不同语境含义可能不太一样
  5. 阻塞和非阻塞是多用于描述进程状态的形容词
  6. 同步和异步是多用于描述交互模式的形容词

下面我们从网络I/O模型的例子中抽象一下这四个词想表达的意思。我的理解是,在数据交互的模式中。如果某一方流程的继续进行需要等待其他资源就绪,那么我们称该流程被阻塞。如果数据请求方流程的继续需要等待生产方返回数据资源,那么我们称请求方流程被阻塞,这种交互模式是同步的。如果数据请求方在请求数据后没有等待数据返回,而是继续进行其他操作,那么这种数据交互模式叫做异步。在数据交互模式中,数据请求方在发起数据请求后是否阻塞的状态是判断交互模式是同步还是异步的依据。所以异步阻塞和同步非阻塞的说法其实是有问题的。

在烧开水的例子中,老张是数据请求方,烧水壶是数据生产方,水是数据。阻塞与非阻塞是老张在等待开水时是否能干别的事,也就是数据请求方是否因为请求数据这件事被阻塞,这点看起来没什么问题。但是将水壶是否会响作为同步异步的依据我认为是有问题的。这就好像是说由谁来通知数据请求方交互完成是同步模式和异步模式的判断依据。但同步异步判断的依据按理来说应该是请求方的整体流程是否阻塞。

不过由数据生产方告诉消费方数据准备完成应该是异步交互模式一种常用的实现模式。比如在某个修改数据的场景,前端调用A服务,A服务先查询B服务,等待B服务的数据然后修改数据。如果A等待B服务返回数据再修改然后再返回前端那么这种就是同步的。而如果A调用B服务时告诉B一个新接口,然后不等B返回直接告诉前端修改成功,等B调用新接口A在完成修改操作,这就是典型的生产方主动告诉请求方数据完成的异步交互。