网络I/O模型
Unix 系统下共有 5 种 I/O 模型:同步阻塞式 I/O、同步非阻塞式 I/O、I/O 多路复用(select 和 poll)、信号驱动式 I/O(SIGIO) 和异步 I/O(POSIX 的 aio_系列函数)
要了解 I/O 模型,首先要知道什么是 I/O。查看 wikipedia,能看到如下解释
I/O(英语:Input/Output),即输入/输出,通常指数据在存储器(内部和外部)或其他周边设备之间的输入和输出,是信息处理系统(例如计算机)与外部世界(可能是人类或另一信息处理系统)之间的通信。输入是系统接收的信号或数据,输出则是从其发送的信号或数据。该术语也可以用作行动的一部分;到“运行I/O”是运行输入或输出的操作。
简单来说,I/O 就是指计算机内存与外部设备之间的数据交互过程。比如说:将磁盘上的文件读入内存中、将内存中的数据写入磁盘。这里所说的外部设备还可以是网卡、显示设备等等。
由于 CPU的速度 > 内存的速度 > 外部设备的速度,所以在 I/O 过程中,就会存在速度不匹配的问题。在发出 I/O 请求后,CPU 可以选择不同的处理策略:可以是继续等待结果;也可以是直接返回,然后不断轮询看数据是否到达;还可以注册一个函数等待数据到达后的回调。不同的策略对 CPU 的利用率有很大的影响。
考虑一次网络 I/O 过程,当用户线程通过系统函数调用(recvfrom、select等)发起 I/O 请求后,需要等待网络数据到达后,内核将数据复制到内核空间,然后再将数据从内核空间复制到用户空间。不同 I/O 模型的主要区别就在于对这两个过程的处理。
IO 模型
同步阻塞式 I/O
用户线程发起 I/O 请求后阻塞,内核等待数据到达后,将数据复制到内核空间,然后再复制到用户空间,完成后将用户线程唤醒
同步非阻塞式 I/O
用户线程把 socket 设置为非阻塞,然后发起 I/O 请求,当数据未准备好时,内核会直接返回一个错误标志。用户线程持续轮询内核,一旦数据数据已准备好,本次请求就需要等待数据从内核空间复制到用户空间,之后会返回一个成功标志
I/O 多路复用
I/O 多路复用是通过 select(或者 poll)管理多个 I/O 请求,当没有数据准备好时,用户线程阻塞在 select 调用上,若某个数据已准备好,则可以发起 recvfrom 系统调用,将数据从内核空间复制到用户空间
I/O 多路复用与同步阻塞式 I/O 有些相似,区别是阻塞式 I/O 中,用户线程是阻塞在一个 I/O 请求上,而 I/O 多路复用中,用户线程是阻塞在 select 调用中,select 本身管理了多个 I/O 请求。
信号驱动式 I/O
通过 sigaction 系统调用向内核注册 SIGIO 处理函数,让内核在数据准备就绪时通知用户线程,用户线程在收到通知后,直接通过 recvfrom 系统调用发起 I/O 请求,将数据从内核空间复制到用户空间。和同步阻塞式 I/O 相比,在等待数据准备好的这段时间,用户线程不用持续轮询内核,可以去处理自己的事情。
异步 I/O
通过 aio_read 系统调用通知内核开启 I/O 操作,内核会完成所有的 I/O 操作,在将数据从内核空间复制到用户空间后,内核会通知用户线程 I/O 已完成。与信号驱动式 I/O 的区别是,异步 I/O 整个过程都是由内核完成的。
总结
一次网络 I/O 可分为两个阶段:发起 I/O 请求、执行 I/O 处理。
对于 I/O 的执行过程,可以再分为两个阶段:等待数据就绪写入内核缓冲区、将数据从内核缓冲区拷贝到用户缓冲区。
最简单的 I/O 模型是 BIO,调用者在发起 I/O 后一直阻塞,直到数据复制到用户缓冲区。缺点是阻塞调用者线程,若要提高并发度,需要创建较多的线程,而线程上下文切换是需要代价的。
在 NIO 模型中,调用者发起 I/O 后直接返回,不会阻塞,需要调用者不断轮询。待数据复制到内核缓冲区后,再次轮询则会阻塞,等待数据复制到用户缓冲区。缺点是在数据未准备好时需要轮询,空轮询会增大额外开销。
在 I/O 多路复用模型中,一个 selector 管理了多个 channel。只有当所有的 channel 上的数据都未准备好时,调用 select 操作才会阻塞,否则可以取到一个准备好数据的 channel 来处理。数据从内核空间复制到用户空间的过程还是会阻塞。这是最常用的一种 I/O 模型。
在信号驱动 I/O 模型中,调用者需要注册信号处理程序。待数据准备好后,内核会调用该处理程序,此时可以将数据从内核空间复制到用户空间。等待数据复制的过程中仍然是阻塞的。
异步 I/O 实现了真正的异步操作,调用者发起请求后由内核处理整个 I/O 过程,待数据从内核空间复制到用户空间后再通知调用者。
- 2022-01-01
转眼又是新的一年,时间过得真是太快了,今天继续来聊聊网络 IO 模型的问题。网络 IO 模型是一个比较抽象的概念,你可以很容易地找到很多介绍网络 IO 模型的文章,也有很多介绍网络 IO 系统调用实现的文章。但是前者往往仅限于抽象的概念介绍,缺少关键的细节。后者则往往介绍过多的细节,让人难以把握住重点。本文将会对比 BIO 和 NIO 两种网络 IO 模型的原理和优缺点,然后通过
recvfrom
系统调用介绍两种 IO 模型在使用上的区别,让你不再被各种高级的类库封装所迷惑。为了防止太多细节干扰你的理解,我将仅挑选关键的代码进行介绍。此外,我会在文章末尾贴出完整的代码,以便你进行测试和验证。