同步/异步、阻塞/非阻塞

  阻塞/非阻塞、同步/异步,每次被问到这个问题都感觉自己清楚但是又说的不好,知道他们两个有区别但当真正去描述的时候又说不出来他俩的区别,准备特别写一篇描述一下,加深一下自己的印象。

IO 概念区分

四个相关概念:

  • 同步(Synchronous)
  • 异步( Asynchronous)
  • 阻塞( Blocking )
  • 非阻塞( Nonblocking)

  这四个概念的含义以及相互之间的区别与联系,一直让人难以确定的分辨,这篇博客只能代表本人在此时此刻对与该知识的认知,不确定后期会不会改变,但是还是要学,仅上。

例如曾经看到的解释如下:
同步/异步

  • 同步/异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) 。
  • 所谓同步,就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。
  • 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果

阻塞/非阻塞

  • 阻塞/非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

  上面的解释乍一看大概的解释是 同步/异步阻塞/非阻塞 用在描述两个不同的维度,可以区别对待,但是稍微推敲一下发现它俩还是有问题。

  • 首先,如果 "同步" 是发生了一个调用后,没得到结果之前不返回,那他毫无疑问就是被 “阻塞” 了(即调用进程处于 “waiting” 状态)
  • 其次,如果 “异步” 调用发出以后直接返回了,毫无疑问,这个进程没有被 “阻塞”
      所以上面的解释是不正确的,或者说是不完善的。让我们看一下《操作系统概念(第九版)》中有关进程间通信的部分是如何解释的:
    image.png
    image.png
    翻译一下就是:

进程间的通信是通过 send() 和 reveive() 两种基本操作完成的。具体如何实现这两种基础操作,存在着不同的设计。消息传递有可能是阻塞的或者是非阻塞的-也被称为同步异步的。

  • 阻塞发送(blocking send):发送方进程会被一直阻塞,直到消息被接收方进程收到
  • 非阻塞发送(nonblocking send):发送方进程调用 send() 后,立即就可以其他操作。
  • 阻塞接收(blocking receive):接收方调用 reveive() 后一直阻塞,直到消息到达可用
  • 非阻塞接收(nonblocking receive):接收方调用 reveive() 函数后,要么得到一个有效的结果,要么得到一个空值,即不会被阻塞

  上述不同类型的发送方式和不同类型的接收方式,可以自由组合。 也就是说, 从进程级通信的维度讨论时, 阻塞和同步(非阻塞和异步)就是一对同义词, 且需要针对 发送方接收方 作区分对待。


下面对理解同步异步,阻塞非阻塞所需的知识点进行详细叙述

用户空间和内核空间

  操作系统为了支持多个应用同时运行,需要保证不同进程之间相对独立(一个进程的崩溃不会影响其他的进程,恶意进程不能直接读取和修改其他进程运行时的代码和数据)。因此操作系统内核需要拥有高于普通进程的权限,以此来调度和管理用户的应用程序。

  于是内存空间被划分为两部分,一部分为内核空间,一部分为用户空间,内核空间存储的代码和数据拥有更高级别的权限。内存访问的相关硬件在程序执行期间会进行访问控制(Access Control),使得用户空间的程序不能直接读写内核空间的内存。

进程切换

image.png

上图展示了进程切换中几个最重要的步骤:

  1. 当一个程序正在执行的过程中,中断(interrupt)或 系统调用(system call)发生可以使得 CPU 的控制权会从当前进程转移到操作系统内核
  2. 操作系统内核负责保存进程 $i$ 在 CPU 中的上下文(程序计数器,寄存器等)到 $PCB_i$(操作系统分配给进程的一个内存块)中。
  3. 从$PCB_j$ 取出进程 $j$ 的 CPU 上下文,将 CPU 控制权转移给进程 $j$,开始执行进程 $j$ 的指令。

为了下文的解释,引入几个前置芝士

  • 中断(interrupt)
    • CPU 微处理器有一个中断信号位,在每个 CPU 时钟周期的末尾, CPU 会去检测那个中断信号位是否有中断信号到达,如果有,则会根据中断优先级决定是否要暂停当前执行的指令,转而去执行处理中断的指令。(其实就是 CPU 层级的 while 轮询)
  • 时钟中断( Clock Interrupt )
    • 一个硬件时钟会每隔一段时间(很短)的时间就产生一个中断信号发送给 CPU, CPU 在响应这个中断时,就会去执行操作系统内核的指令,继而将 CPU 的控制权转移给了操作系统内核,可以由操作系统内核决定下一个要被执行的指令。
  • 系统调用(system call)
    • system call 时操作系统提供给应用程序的接口。用户通过调用系统调用来完成那些需要操作系统内核进行的操作,例如键盘、硬盘、网络接口设备的读写等等。

  从上述描述中, 可以看出来, 操作系统在进行进切换时,需要进行一系列的内存读写操作, 这带来了一定的开销。对于一个运行着 UNIX 系统的现代 PC 来说, 进程切换通常至少需要花费 300 us 的时间

进程阻塞

image.png

上图展示了一个进程不同的状态:

  • New:进程正在被创建.
  • Running:进程的指令正在被执行
  • Waiting: 进程正在等待一些事件的发生(例如 I/O 的完成或者收到某个信号)
  • Ready:进程在等待被操作系统调度
  • Terminated:进程执行完毕(可能是被强行终止的)

  我们所说的“阻塞”是指进程在发起了一个系统调用后,由于该系统调用的操作不能立即被完成,需要等待一段时间,于是内核将该进程挂起为**等待(waiting)**状态,以确保它不会被调度执行,占用 CPU 资源。因为在任意时刻,一个 CPU 核心上只能运行一个进程

I/O System Call 的阻塞/非阻塞, 同步/异步

  这里再重新审视 阻塞/非阻塞 IO 这个概念, 其实阻塞和非阻塞描述的是进程的一个操作是否会使得进程转变为“等待”的状态, 但是为什么我们总是把它和 IO 连在一起讨论呢?

  原因是,阻塞这个词是与系统调用紧密联系在一起的,因为要让一个进程进入**等待(waiting)**状态,要么是它主动调用 wait() 或者 sleep() 等主动挂起自己的操作,要么就是它调用 System Call,而System Call因为涉及 I/O 操作,不能立即完成,于是内核就会先将该进程置为等待状态,调度其他进程的运行,等到它所请求的 I/O 操作完成后,再将其状态改回 **就绪(ready)**态。

  操作系统内核在执行 System Call 时,CPU 需要与 IO 设备完成一系列物理通信上的交互,其实再一次会涉及到阻塞和非阻塞的问题。例如,操作系统发起了一个读硬盘的请求后,其实是向硬盘设备通过总线发出来一个请求,它既可以阻塞式的等待 IO 设备的返回结果,也可以非阻塞式的继续其他的操作。在现代计算机中,这些物理通信操作基本都是异步完成的。即发出请求后,等待 I/O 设备的中断信号后,再来读取享应的设备缓冲区。但是,大部分操作系统默认用户级应用程序提供的都是阻塞式系统调用(blocking systemcall)接口,因为阻塞式的调用,使得应用级代码的编写更加容易(代码的执行顺序和编写顺序是一致的)。

  但同样, 现在的大部分操作系统也会提供非阻塞 I/O 系统调用接口(Nonblocking I/O system call)。 一个非阻塞调用不会挂起调用程序, 而是会立即返回一个值, 表示有多少bytes 的数据被成功读取(或写入)。

默认情况下,所有的IO都是阻塞的。

I/O 方式对比

根据 《UNIX网络编程:卷一》第六章——I/O复用,书中一共提到了 五种类UNIX下可用的 I/O 模型:

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O复用(select,poll,epoll...)
  • 信号驱动式I/O(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)

一个输入操作通常包含两个不同的阶段:

  1. 等待数据准备好
  2. 从内核向进程复制数据

根据书中的总结

image.png

  其实前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理。而且同步 I/O 在 Wiki上是查不到的,甚至无法在 Google 上搜索这个词条,同步 I/O 的范围太广了。

总结

  用我自己的话总结一下,想要较好的解释这个问题(阻塞/非阻塞、同步/异步),应该分情况回答

  • 在进程通信层面,阻塞/非阻塞,同步/异步基本是同义词,但是需要注意区分讨论的对象是发送方还是接收方
    • 发送方阻塞/非阻塞(同步/异步)和接收方阻塞/非阻塞(同步/异步)是互不影响的。
  • 在 I/O 系统调用层面,同步 I/O 包括 阻塞 I/O 以及 非阻塞 I/O 。但是 非阻塞 IO 系统调用异步 IO 系统调用存在着一定的差别, 它们都不会阻塞进程, 但是返回结果的方式和内容有所差别, 但是都属于非阻塞系统调用( non-blocing system call )