网络 IO 模型

Posted by icoding168 on 2020-03-30 00:01:53

分类: 网络编程  

网络 IO 模型有阻塞 IO、非阻塞 IO、IO 多路复用、信号驱动 IO、异步 IO 等,其中前面 4 个都属于同步 IO。

IO 模型涉及到的系统对象主要有两个,一个是系统内核,另一个是应用程序:

Socket 读取数据要经历两个阶段:

  • 等待从网络传过来的数据保存到内核的缓冲区中
  • 将内核中的数据复制到应用程序的缓冲区中

阻塞和非阻塞

一个程序调用其它程序,调用过程中调用者必须等待被调用者完成执行自身的指令后才能做其他事情,这就是阻塞;假如被调用者没有完成执行自身的指令就立即返回,调用者通过定时轮询的方式获取被调用者的执行结果,这就是非阻塞。

同步和异步

一个程序调用其它程序,调用过程中必须等待当前步骤完成才能进行下一步就是同步,否则就是异步。

⚠️ 阻塞/非阻塞、同步/异步的概念要注意讨论的上下文 ,在进程通信层面, 阻塞/非阻塞、同步/异步基本是同义词;在 IO 系统调用层面( IO system call ), 非阻塞 IO 和异步 IO 都不会阻塞进程,都属于非阻塞系统调用( non-blocking system call ),但是返回结果的方式和内容有所差别。

下面以 Socket 读取数据为例介绍 IO 模型。

阻塞 IO

最常见的 IO 模型就是阻塞 IO,默认情况下 Socket 就是阻塞的。

当应用程序从 Socket 读取数据时,只要数据还没有被完整复制到应用程序中,应用程序就会一直处于阻塞状态,这就是阻塞 IO 。

在阻塞状态下,应用程序并不占用 CPU 计算资源,这带来了一个问题,部分计算资源处于闲置状态,无法被充分利用。为了提高资源利用率,可以使用多线程或者多进程处理网络连接。

非阻塞 IO

将 Socket 设置为非阻塞后,当应用程序从 Socket 读取数据时,如果内核中的数据还没有准备好,内核不会阻塞应用程序,而是立刻返回一个错误信息,这个错误信息是告诉应用程序数据还没有准备好,应用程序可以不断地主动询问内核数据好了没有,这就是非阻塞 IO。

非阻塞 IO 不断轮询的方式会浪费 CPU 计算资源, 特别是网络连接数量较多的情况下。

IO 多路复用

在 IO 多路复用中,Socket 依然设置为非阻塞,与非阻塞 IO 不同的是当应用程序从 Socket 读取数据时,需要先调用选择器,由选择器代替应用程序向 Socket 轮询数据是否准备完毕,应用程序在数据准备好之前会一直处于阻塞状态。

IO 多路复用可以用单线程同时处理多个网络连接,但这不代表 IO 多路复用的性能一定比使用多线程的阻塞 IO 更强,IO 多路复用的优势在于能处理更多的网络连接。

信号驱动 IO

应用程序发起 IO 系统调用后,内核立即返回,应用程序可以继续执行,也就是说等待数据阶段应用程序是非阻塞的。内核在数据到达时向应用程序发送 SIGIO 信号,应用程序收到之后在信号处理程序中将数据从内核复制到应用进程中。信号驱动 I/O 大量 IO 操作时可能会因为信号队列溢出导致没法通知,这种 IO 模型很少被使用。

异步 IO

异步 IO 的基本流程是:应用程序通过系统调用,向内核注册某个 IO 操作。内核在整个 IO 操作(包括数据准备、数据复制)完成后,通知应用程序。在内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,应用程序都不会被阻塞。

目前只有 Windows 系统下通过 IOCP 实现了真正的异步 IO,Linux 系统下的异步 IO 在 2.6 版本才引入,目前还不完善,在性能上与 IO 多路复用相比没有明显的优势。大多数高并发网络应用程序都是基于 Linux 系统开发的,而且基本都采用了 IO 多路复用模型,例如 Netty、Redis 等。