网络 IO 模型:同步异步,傻傻分不清楚?

2019-08-14 16:52:27 浏览数 (2)

阻塞 IO, 非阻塞 IO, 同步 IO, 异步 IO 这些术语相信有不少朋友都也不同程度的困惑吧? 我原来也是, 什么同步非阻塞 IO, 异步非阻塞 IO 的, 搞的头都大了.。后来仔细读了一遍。

《UNIX 网络编程卷一 套接字联网 API(第三版)》的 6.2 章节, 终于把这些名词搞懂了。

下面我以《UNIX 网络编程卷一 套接字联网 API(第三版)》的 6.2 章节的内容为准, 整理了一下各种网络 IO 模型具体定义以及一些容易混淆的地方。

简介

Unix 下有 5 种可用的 IO 模型, 分别是:

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

阻塞式 I/O 模型

最流行的 IO 操作是阻塞式 IO(Blocking IO). 以 UDP 数据报套接字为例, 下图是其阻塞 IO 的调用过程:

在上图中, 进程调用 recvfrom, 其系统调用直到数据报返回并且被复制到应用进程的缓冲区中 或者发送错误时才返回. 因此进程在调用 recvfrom 开始到它返回的整段时间内都是被阻塞的。

非阻塞式 IO(Non-Blocking IO)

进程把一个套接字设置为非阻塞是在通知内核: 当调用线程所请求的 IO 操作需要调用线程休眠来等待操作完成时, 此时不要将调用线程休眠, 而是返回一个错误。

如上图所示, 前三次调用 recvfrom 时, 没有数据可返回, 因此内核转而立即返回一个 EWOULDBLOCK 错误. 第四次调用 recvfrom 时, 已经有数据了, 此时, recvfrom 会阻塞住, 等待内核将数据赋值到应用进程的缓冲区中, 然后再返回.(注意, 当有数据时, recvfrom 是阻塞的, 它会等待内核将数据复制到应用进程的缓冲区后, 才返回)。

当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom 时, 我们称之为轮询(polling). 应用进程持续轮询内核, 以查看某个操作是否完成, 这么做会消耗大量的 CPU 时间, 不过这种模型偶尔也会遇到, 通常是专门提供某一种功能的系统中才有。

IO 复用模型

有了 IO 复用(IO multiplexing), 我们就可以调用 select 或 poll, 阻塞在这两个系统调用中的某一个之上, 而不是阻塞在真正的 IO 系统调用上. 例如:

如上图所示, 当调用了 select 后, select 会阻塞住, 等待数据报套接字变为可读. 当 select 返回套接字可读这一条件时, 我们就可以调用 recvfrom 把所读取的数据报复制到应用进程缓冲区。

对比阻塞式 IO, IO 复用模型优势并不明显, 并且从使用方式来说, IO 复用模型还需要多调用一次 select, 因此从易用性上来说, 比阻塞式 IO 还略有不足. 不过 select 的杀手锏在于它可以监听多个文件描述符, 大大减小了阻塞线程的个数。

信号驱动 IO 模型

信号驱动模型如上图所示. 当文件描述符就绪时, 我们可以让内核以信号的方式通知我们。

我们首先需要开启套接字的信号驱动式 IO 功能, 并通过 sigaction 系统调用安装一个信号处理函数. sigaction 系统调用是异步的, 它会立即返回. 当有数据时, 内核会给此进程发送一个 SIGIO 信号, 进而我们的信号处理函数就会被执行, 我们就可以在这个函数中调用 recvfrom 读取数据。

异步 IO 模型

异步 IO (asynchronous IO) 由 POSIX 规范定义, 在 POSIX 中定义了若干个异步 IO 的操作函数. 这个函数的工作原理是: 告知内核启动某个动作, 并让内核在整个操作(包括将数据从内核复制到应用进程缓冲区)完成后通知我们的应用进程。

异步 IO 模型和信号驱动的 IO 模型的主要区别在于: 信号驱动 IO 是由内核通知我们何时可以启动一个 IO 操作, 而异步 IO 模型是由内核通知我们 IO 操作何时完成。

异步 IO 模型的操作过程如图所示:

当我们调用 aio_read 函数时(POSIX 异步 IO 函数以 aio_或 lio_ 开头), 给内核传递描述符, 缓冲区指针, 缓冲区大小(和 read 相同的三个参数) 和文件偏移(以 lseek 类似), 并告诉内核当整个操作完成时如何通知应用进程. 该系统调用立即返回, 而且在等待 IO 完成期间, 应用进程不被阻塞。

各种 IO 模型的比较

如图所示, 上述五中 IO 模型中, 前四种模型(阻塞 IO, 非阻塞 IO, IO 复用, 信号驱动 IO)的主要区别在于第一阶段, 因为他们的第二阶段是一样的: 在数据从内核复制到调用者的缓冲区期间, 进程阻塞于 recvfrom 调用. 而第五种, 即异步 IO 模型中, 两个阶段都不需要应用进程处理, 内核为我们处理好了数据的等待和数据的复制过程.

关于同步 IO 和异步 IO

根据 POSIX 定义:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes(导致请求进程阻塞, 直到 IO 操作完成)。
  • An asynchronous I/O operation does not cause the requesting process to be blocked(不导致请求进程阻塞)。

根据上述定义, 我们的前四种模型: 阻塞 IO 模型, 非阻塞 IO 模型, IO 复用模型和信号驱动 IO 模型都是同步 IO 模型, 因为其中真正的 IO 操作(recvfrom 调用) 会阻塞进程(因为当有数据时, recvfrom 会阻塞等待内核将数据从内核空间复制到应用进程空间, 当赋值完成后, recvfrom 才返回.) 只有异步 IO 模型与 POSIX 定义的异步 IO 相匹配。

总结

在处理网络 IO 操作时, 阻塞和非阻塞 IO 都是同步 IO。

只有调用了特殊的 API 才是异步 IO。

因此网上常说的 "同步阻塞 IO", "同步非阻塞 IO" 其实就是阻塞 IO 模型和非阻塞 IO 模型, 因为阻塞 IO 和非阻塞 IO 模型都是同步的, 加了 "同步" 二字其实是多余了。

网络上常说的 "异步非阻塞 IO" 其实就是异步 IO 模型。

0 人点赞