0%

阻塞和非阻塞、同步和异步的概念详解

我个人对于网络编程有着强烈的兴趣,最近打算开始看 muduo 的网络库,了解到其构建了基于 Reactor 的事件处理机制,本质涉及 I/O 多路复用的知识。为了后续的理解,首先搞清楚一个有无数讲解却又令人费解的概念:阻塞和非阻塞、同步和异步。

首先需要搞清楚的一件事,就是对于 Linux 系统, I/O 操作不是一步完成的。此处的 I/O 操作是一个通用型的概念,对于 socket 通信,也可以看作一个 I/O 操作过程,只不过操作的是网络对象。

I/O 操作一般分为两个部分:

  • 应用程序发起 I/O 操作请求,等待数据,或者将要操作的数据拷贝到系统内核中(比如 socket)。
  • 系统内核进行 I/O 操作(一般是内核将数据拷贝到用户进程中)。

了解了这个大前提,我们再来看上述四个概念

阻塞和非阻塞

首先明确一点:阻塞和非阻塞发生在请求处,关注的是程序在等待调用结果时的状态

上面的概念非常重要,很明确地指出了阻塞的要点,应用程序进程(线程)进行 I/O 请求被阻塞了。很容易理解下面的结论:

  • 阻塞调用是指调用结果返回之前,当前进程(线程)会被挂起。调用进程(线程)阻塞在 I/O 操作请求处,直到 I/O 操作请求完成,数据到来,最重要的是用户进程的函数在请求的过程中不会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前进程(线程),进程(线程)可以去干别的事情。一般使用轮询的方式来查询 I/O 操作数据是否准确好了。

理解上面概念的一个要点是请求的结果是否立即返回,同时需要注意的是,结果立即返回,不代表 I/O 操作完成,阻塞和非阻塞只关注请求是否立即获得结果。

默认的 socket 是阻塞的,用户进程发起 system call 之后,内核等待数据从 socket 接收,用户进程等待内核拷贝数据回用户缓冲区,整个过程中,进程调用函数是阻塞在调用处的,一直到数据拷贝到用户进程的缓冲区,返回数据的长度,调用完成,继续执行。

当使用 I/O 多路复用的时候,用户的 I/O 操作会立即返回,但会利用 select 和 epoll 等方法对所监视的 I/O 操作描述符进行遍历轮询(此操作是为了检查数据是否准备完毕,也就是 I/O 操作的第一部分,同时此操作是阻塞,进程或者线程需要等待轮询结果的返回),查看可用的句柄并返回。然后用户进程再对其进行操作。

I/O 操作是否阻塞,可以通过其文件描述符的状态来设定。

同步和异步

同样需要明确一点:同步和异步关注的是消息通信机制,具体来说就是调用者是否等待调用结果的返回,对于 I/O 操作而言,就是应用程序是否等待 I/O 操作完成

注意:此处的 I/O 操作一般是指上文中 I/O 操作中的两部分的第二部分。

这一点该怎么理解呢?还是得结合上述的 I/O 操作的两步来理解。同步和异步其实就是指 I/O 操作的第二部分,也就是进行具体 I/O 操作过程中,用户进程是否等待 I/O 操作结果返回。

结合前面的概念,可以看出,其实这两者存在本质的区别,它们的修饰对象是不同的

  • 阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。
  • 同步和异步是指访问数据的机制,同步一般指主动请求并等待 I/O 操作完毕的方式,当数据就绪后在读写的时候必须等待,异步则指主动请求数据后便可以继续处理其它任务,随后等待 I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。

网上有一个银行办业务的例子说的很明白:

你去银行办业务,前面有人(数据未就绪),你可以自己排队(阻塞),也可以拿号然后干别的(非阻塞),然后隔一会询问一下是否到你了(轮询)。你可以自己去银行办这些事(同步),如果你工作比较忙,也可以委托银行的经理帮你办理这些事,然后告诉你结果(异步)。所以说只有同步的时候,才会有是否阻塞之说

I/O 模型

根据上述概念的组合,我们可以将常见的 I/O 操作归属不同的 I/O 模型:

  • 同步模型(synchronous I/O )
    • 阻塞 I/O (bloking I/O )
      • 默认socket的 I/O 操作是阻塞的
    • 非阻塞 I/O (non-blocking I/O )
      • 当 I/O 请求时,如果数据没有准备好,则返回一个错误,然后进程可以干其他事,隔段时间,重新请求,直到数据准备好,进入 I/O 操作,等待操作结果返回。
    • 多路复用 I/O (multiplexing I/O )
      • 利用select和epoll等函数同时监视多个socket,本质上是非阻塞 I/O ,但这些监视函数在轮询时是阻塞的,因此将 I/O 请求阻塞在这些监视函数上了,用户进程不需要轮询数据是否准备好了。
    • 信号驱动式 I/O (signal-driven I/O )
      • 系统会为监视的数据准备设置信号,用户进程进行 I/O 请求之后,不会阻塞,当内核数据准备完毕,会通过信号通知用户进程,随后进行 I/O 操作。
  • 异步 I/O (asynchronous I/O )
    • 异步 I/O
      • 向内核中传递 I/O 操作参数,并立即返回。内核 I/O 操作完毕后,通过回调函数的方式通知用户进程。

关于这些 I/O 模型的概念,网上就太多重复的了,此处给出一篇参考:I/O 模型详解