四种常见网络IO模型

网络IO模型一共有五种,同步阻塞IO、同步非阻塞IO、IO多路复用、信号驱动IO和异步IO。其中信号驱动IO并不常用,我们只要介绍其他四种。

首先我们需要了解一下网络IO的基本原理和一些基本的概念。

IO读写的基本原理

用户程序进行IO操作,依赖于底层的IO实现,主要是底层的read&write两大系统调用。read系统调用,并不直接从物理设备把数据读取到内存中,而是将数据从内核缓冲区复制到进程缓冲区;write系统调用,也不是直接把数据写入到物理设备中,而是把数据从进程缓冲区复制到内核缓冲区。

这也就是说,上层程序的IO操作,实际上不是物理设备的级别的读写,而是缓存的复制。

在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程)都有自己独立的缓冲区,叫做进程缓冲区吧。用户程序的IO操作,大部分情况下,并没有进行实际的IO操作,而是进程缓冲区和内核缓冲区之间直接进行数据交换。如下图所示:
四种常见网络IO模型

以read系统调用为例,一个完整的输入流程分为两个阶段:

  • 等待数据准备好。
  • 从内核想进程复制数据。

具体一点,如果是在Java服务器端,完成一个Socket请求和详情,完整的流程如下:

  • 客户端请求:Linux通过网卡读取客户端请求数据,将数据读到内核缓冲区。
  • 获取请求数据:Java服务器通过read系统调用,从linux内核缓冲区读取数据,在送入Java进程缓冲区。
  • 服务端业务处理:Java服务器在自己的用户空间中处理客户端的请求。
  • 服务端返回数据:Java服务器完成处理后,构建好响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到write系统调用。
  • 发送给客户端:Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡底层通信协议,会将数据发送给目标客户端。

四种主要的IO模型

在介绍IO模型前,我们需要明白几个概念。

阻塞和非阻塞:阻塞IO,指的是需要内核IO操作彻底完成后,才返回用户空间执行用户的操作。非阻塞IO指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,于此同时内核会立即返回给用户一个状态值。换句话说就是,阻塞就是用户空间(调用线程)死等内核IO,这期间什么都不干。非阻塞是用户空间(调用线程)拿到内核返回的状态就直接返回自己的空间,IO操作可以干就干,不可以干就去干别的事情。

同步和异步: 同步和异步指的是用户空间和内核空间IO发起的方式。同步IO只是用户空间的线程是主动发起IO的一方,内核空间是被动接受方。异步IO则是反过来,系统内核是主动发起IO请求的一方,用户空间的线程是被动接受方。

同步阻塞IO(Blocking IO)

在同步阻塞IO模型中,程序用IO调用开始,直到系统调用返回,在这段时间内,进程是阻塞的。直到返回成功后,应用程序开始处理用户空间的缓存区数据。

主要分为两个阶段:

  • 等待数据就绪。网络IO就是等待远端数据陆续到达;磁盘IO就是等到磁盘数据从磁盘读取到内核缓冲区。
  • 数据复制。用户空间的程序没有权限直接读取内核缓冲区的数据(操作系统处于安全的考虑),因此内核与需要把内核缓冲区的数据复制一份到进程缓冲区。

同步阻塞IO模型如下图所示:
四种常见网络IO模型

阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了。

同步非阻塞IO(None Blocking IO)

将Socket设置为NONBLOCK,当前连接就变成了非阻塞IO。使用非阻塞模式的IO读写,叫做同步非阻塞IO(None Blocking IO),简称NIO模型(需要注意,这里的NIO和Java里面的NIO不是一个东西!)。

在同步非阻塞IO模型中,会出现下面几种情况:

  1. 在内核缓冲区没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。这样请求就不会阻塞。
  2. 用户线程需要不断的发起IO系统调用,测试内核数据是否准备好。
  3. 内核数据准备好以后,用户线程发起系统抵用,用户线程阻塞。内核开始将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果。直到这时,用户线程读到了数据,才会接触阻塞状态,重新运行起来。

同步非阻塞IO模型如下图所示:
四种常见网络IO模型

同步非阻塞IO特点:程序需要不断的进行IO系统调用,轮询数据是否准备好,如果没有准备好,就继续轮询。

IO多路复用模型(IO Multiplexing)

在IO多路复用模型中,引入了一种新的系统调用select/epoll,查询IO的就绪状态。通过该系统调用可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓存区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。

IO多路复用模型如下图所示:
四种常见网络IO模型

IO多路复用模型的特点:IO多路复用模型涉及两种系统调用,一种是就绪查询(select/epoll),一种是IO操作。

和NIO模型相似,多路复用IO也需要轮询。负责就绪状态查询系统调用的线程,需要不断的进行select/epoll轮询,查找出达到IO操作就绪的socket连接。

异步IO模型(Asynchronous IO)

异步IO模型(Asynchronous IO)简称AIO,其基本流程时:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,执行后续的业务操作。

在异步IO模型中,整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓存区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

异步IO模型如下图所示:
四种常见网络IO模型

异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。当内核的IO操作(等待数据和复制数据)全部完成后,内核会通知应用程序读数据。

四种IO模型的优缺点

同步阻塞IO

优点:程序开发简单;在阻塞等待数据期间,用户线程挂起,不占用CPU资源。

缺点:需要为每一个连接的IO配备一个线程,在高并发应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会十分巨大。

同步非阻塞IO

优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

缺点:需要不断的进行系统调用轮询,测试数据是否准备好,将占用大量的CPU时间,效率低下。

IO多路复用模型

优点:使用一个查询就绪状态的线程就可以同时同时轮询成千上万个连接。系统不必要创建和维护大量的线程,大大减小了系统的开销。

缺点:select/epoll调用都是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是这个读写过程时阻塞的。

异步IO

优点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。

缺点:需要操作系统底层内核提供支持。