如果这篇文章有帮到你,能给我一个 star 吗🥰 👉 github.com/night-cruis…
IO 访问
对于一次 IO 访问(例如 read 操作),通常有两个不同的阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
例如在一个 socket 上读取数据,首先需要等待数据到达网络,当数据到达时将数据拷贝到内核缓冲区中,再将数据从内核缓冲区中拷贝到用户进程的缓冲区中。
正是由于 IO 访问经历的两个阶段,Linux 系统产生了下面五种 IO 模型:
- 阻塞 IO(blocking IO)
- 非阻塞 IO(nonblocking IO)
- IO 多路复用(IO multiplexing)
- 信号驱动 IO(signal driven IO)
- 异步 IO(asynchronous IO)
IO 模型与 Future
在介绍 Future trait 的那一章中我们提到:如果一个 Future 没有计算完成,例如想要等待一个 IO 事件发生,那么通常会注册 waker 到一个“事件通知系统”中,当这个 IO 事件就绪时,“事件通知系统”就会通过 waker 唤醒之前的 Future 继续执行。
那么“事件通知系统”要怎么知道 Future 想要等待的 IO 事件什么时候就绪呢?这与 IO 模型有关,因此在本章中我们将会介绍几种不同的 IO 模型以及它们的特点。
阻塞 IO
在 Linux 中,阻塞 IO 是最流行的 IO 模型,默认情况下所有的 socket 都是阻塞的(blocking)。对于阻塞 IO 来说,读操作的流程如下所示:
当用户进程发起 recvfrom 系统调用后,内核开始 IO 的第一个阶段:等待数据准备好,把数据从硬件拷贝到内核缓冲区(对于网络 IO,要先等待数据报文到达)。当数据准备好后,开始 IO 的第二个阶段:把数据从内核缓冲区拷贝到用户进程的缓冲区。当两个 IO 阶段都完成后,recvfrom 系统调用返回,也就是说用户进程从发起 recvfrom 系统调用直到返回都是处于阻塞状态。
因此,对于阻塞 IO 来说,用户进程在 IO 的两个阶段都被 recvfrom 系统调用阻塞了。
非阻塞 IO
在 Linux 中,我们可以把一个 socket 设置为非阻塞(nonblocking)。对于非阻塞 IO 来说,读操作的流程如下所示:
当用户进程发起 recvfrom 系统调用后,如果数据没有准备好,recvfrom 系统调用会立即返回 EWOULDBLOCK 错误。用户进程可以通过一个死循环不断发起 recvfrom 系统调用,一旦数据准备好了,就进入 IO 的第二个阶段:把数据从内核缓冲区拷贝到用户用进程的缓冲区,当拷贝完成后,recvfrom 系统调用正常返回。
因此,对于 Nonblocking IO 来说,用户进程需要不断轮询内核数据准备好了没有,并且用户进程在 IO 的第二个阶段仍然会被 recvfrom 系统调用阻塞。
信号驱动 IO
对于信号驱动 IO 来说,读操作的流程如下所示:
当用户进程发起 sigaction 系统调用后,这个系统调用会马上返回。内核在准备好数据后会向用户进程发送 SIGIO 信号,用户进程收到信号之后会在信号处理程序中发起 recvfrom 系统调用将数据从内核缓冲区复制到用户进程缓冲区中,至此 IO 的两个阶段全部完成。
因此,对于信号驱动 IO 来说,用户进程在 IO 的第二个阶段被 recvfrom 系统调用阻塞了。
IO 多路复用
IO 多路复用是指通过一种机制实现在单个线程中可以监视多个文件描述符(例如 socket 描述符),当文件描述读/写就绪时,用户进程就可以获取就绪的文件句柄。select、poll、epoll 都是 IO 多路复用的一种实现。
以 select 为例,读操作的流程如下所示:
当用户进程发起 select 系统调用后,用户进程被阻塞,而内核会监控 select 负责的所有文件描述符,当任意一个文件描述符的数据准备好时,select 会返回就绪的文件描述符。此时,用户进程就可以对就绪的文件描述符发起 recvfrom 系统调用,开始 IO 的第二个阶段:将数据从内核缓冲区拷贝到用户进程的缓冲区,当拷贝结束后 recvfrom 调用正常返回。
因此,对于 IO 多路复用来说,用户进程在 IO 的两个阶段都被阻塞了:在 IO 的第一个阶段被 select 系统调用阻塞,在 IO 的第二个阶段被 recvfrom 系统调用阻塞。
异步 IO
对于异步 IO 来说,读操作的流程如下所示:
当用户进程发起异步框架 AIO 提供的 aio_read 系统调用后,这个系统调用会马上返回。内核会准备好数据然后把数据从内核缓冲区拷贝到用户进程缓冲区,当 IO 的两个阶段都完成后,内核会发送一个信号通知用户进程 read 操作完成了。
因此,对于异步 IO 来说,用户进程在 IO 的两个阶段都不会被阻塞。
总结
POSIX 对同步 IO 和异步 IO 的定义如下:
- 同步 IO 操作会导致发起请求的进程被阻塞,直到 IO 操作完成。
- 异步 IO 操作导致发起请求的进程被阻塞。
根据 PISIX 的定义,可以把 IO 模型分为以下两类:
最后,各个 IO 模型的比较如下所示:
本书上一个章节:async/await
本书下一个章节:Epoll