Java NIO学习与记录(五): 操作系统的I/O模型

在开始介绍NIO Reactor模式之前,先来介绍下操作系统的五种I/O模型,了解了这些模型,对理解java NIO会有不小的帮助。

前言:一次网络请求的流程

先来看下一个服务端处理一次网络请求的流程图:

图1

一、图1解析

1.1:内核空间&用户空间

内核空间:指操作系统运行时用于程序调度、虚拟内存的使用或者连接硬件资源的程序逻辑。

用户空间:应用程序能够申请使用的空间。

操作系统采用虚拟存储器,操作系统核心是内核(Kernel),独立于普通应用程序,它既可以访问受保护的内存空间,又有访问底层硬件设备的所有权限,为了保证内核安全,使得用户进程不直接操作内核,因此操作系统将虚拟存储器分为两个部分:内核空间&用户空间

1.2:网络请求流程

根据图1,客户端发起一个请求到服务端,请求首先到达的是服务端网卡(步骤1),然后将请求数据copy到内核空间的内核缓冲区内(步骤2),到这一步,我们说一个数据报已经准备好了。

用户空间里的web服务进程(我们真正的业务程序)发起读取内核缓冲区里的数据,若内核缓冲区准备好了数据报,则会将数据报由内核缓冲区copy到用户空间的web服务进程内(步骤3),然后拿着这些数据进行逻辑处理(步骤4),然后将处理结果copy到内和缓冲区(步骤5),然后内核缓冲区将该数据copy到网卡(步骤6),然后远程传输给客户端(步骤7),这就完成了一次网络请求-响应处理。

这里需要指出步骤3下面这一步,这一步没有计入步骤,但这一步恰好是理解I/O是否发生阻塞的关键,下面介绍阻塞/非阻塞IO时会详细讲。

1.3:套接字(socket)&文件描述符(fd)

TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字(socket),套接字提供了很多供应用程序使用的API,比如acceptreadwrite等。

文件描述符(fd),Unix/Linux系统下,其作为一个socket的句柄,可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句柄,通常也用表示文件句柄的fd来表示。

二、I/O模型

2.1:阻塞&非阻塞调用

阻塞与非阻塞的概念是针对调用方(一般指我们的业务程序,如图1中的web服务器进程)来说的。

阻塞调用图1步骤1、2执行期间,没有数据到达内核缓冲区,这个时候web服务器进程发起的获取数据的请求会被直接阻塞,当前相关线程会被挂起,直到步骤1、2完成,有数据写入内核缓冲区,这个时候才会唤醒线程执行步骤3和4.

非阻塞调用:与阻塞调用相反,当没有数据到达内核缓冲区时,web服务发起的获取数据的请求不会发生阻塞,相关线程可以选择做其他事情,然后轮询着查询请求结果即可,当某次轮询出结果,则进行步骤3和4的操作。

2.2:同步&异步处理

同步与异步的概念是针对被调用方(一般是指内核空间里的IO处理,如图1中的步骤1、2)来说的(一定要区分和理解阻塞/非阻塞同步/异步这两个概念)。

同步处理:被调用方得到最终处理结果才返回给调用方。

异步处理:被调用方不用得到结果,只需返回一个状态给调用方,然后开始IO处理,处理完了就主动返回通知调用方。

2.3:数据输入的两个阶段

一个网络输入流程包含下面两个阶段:

  1. 数据准备(步骤1、2)。
  2. 将准备好的数据从内核空间复制到用户空间步骤3)。

2.4:阻塞IO模型

我们从图1步骤3下面的那次请求开始画图,阻塞式I/O模型处理流程如下:

图2

从上图可以看出,阻塞IO模型是指从应用程序发起从socket获取数据(recvfrom)那一刻起,如果内核里没有准备好的数据报,则直接阻塞应用程序,导致应用程序无法去做别的任何事情,直到数据报准备好,被阻塞的程序才会被唤醒,继续处理下面拿到的数据报。

阻塞IO模型只允许一个线程处理一个连接请求,因此当并发量大的时候,会创建大量线程,线程切换开销很大,导致程序处理性能低下。具体参考BIO模式的服务端实现:[SocketChannel与BIO服务器](/2019/03/08/Java NIO学习与记录(四): SocketChannel与BIO服务器/)

2.5:非阻塞IO模型

同样从应用程序发起获取数据的地方开始画图,非阻塞式I/O模型处理流程如下:

图3

从上图可以看出,非阻塞模式也是相对于调用者的,调用者在发送获取数据的请求时会将对应套接口设置为非阻塞,这样在数据报还未准备好的时候,应用程序就不会被阻塞了,然后应用程序再通过轮询的方式进行询问数据报是否已经准备好,当准备好后停止轮询,接下来的逻辑跟阻塞IO一致。对比可以发现,阻塞与非阻塞都是以调用方的角度看的,而且阻塞与否全在第一个阶段,第二个阶段都是一致的。非阻塞IO虽然不会阻塞应用程序,但是因为需要长时间的轮询,对于CPU来说,将会进行大量无意义的切换,资源利用率较低。

2.6:非阻塞IO-多路复用模型

2.6.1:模型介绍

IO多路复用模型处理流程如下:

图4

从上图可以看出,IO多路复用其实是找了个代理select,帮助监听多个IO通道的状态,某个通道有新状态产生,才触发recvfrom操作,没有新的状态产生,则select会阻塞。注意这里的阻塞,与阻塞IO模型里的不同,阻塞IO模型是指一个IO操作发生的阻塞行为,而这里select可以同时阻塞多个IO通道,也就是说select可能会监听到一个以上的IO通道的状态,直到有数据可读、可写时,才真正触发IO操作的函数。

🌿 多路复用

图4里的多路复用是说利用某个IO函数(这里是指select)同时监听多个IO通道的状态变更,这样应用程序就可以通过一个函数同时监听多个通道的就绪状态(如连接就绪、读就绪),多路复用跟后面要讲的NIO不是同一个概念,它只是一种处理模型,而NIO是一组API,它提供的select函数恰好可以实现这种数据处理模型。

另外一种多路复用是指基于传输层协议(如TCP)的特性来实现的数据流传输方式,根据TCP特性,同一个TCP连接可以同时传输多条数据和接收多条数据,而实现这种多路复用的方式取决于应用层协议(全双工通信的应用层协议,比如HTTP2)。多路是指多个数据流,复用是指复用同一个资源(这个资源放到图4就是指select函数,放到通信方式里就是指TCP连接),可以参考其原始概念:多路复用-百度百科。以及这篇知乎上的回答:IO 多路复用是什么意思?

2.6.2:select、poll、epoll函数

上述三个函数均提供IO多路复用的解决方案,但是它们之间存在差异性,下面会介绍具体的区别:

select:

select 函数监视的fd分为writefdsreadfdsexceptfds三类,调用后select函数会阻塞,直到有fd就绪(可读、可写、或except),或超时(指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到已经就绪的fd

优点:跨平台支持,目前几乎所有的平台都支持。

缺点:单个进程内其监视的fd存在最大限制,一般为1024个(linux32)或者2048个(linux64)。另外一个缺点就是其会不断的轮询fdset,不管存不存在活跃的socket,它都会全部遍历一遍fdset来查找就绪的fd,导致浪费许多CPU的时间去做这件事。最后一个缺点是其可能会维护一个存放大量fd的数据结构,这样会使用户空间和内核空间在传递该结构时复制开销过大。

poll:

本质上和select没有区别,但是它解决了select监视fd个数的限制。

优点:对于监视的fd,采用链表结构存储,无个数限制。

缺点:基本上select有的缺点它都有,其次它还有个特点:水平触发,也就是说poll到的fd没有被处理掉,下次依旧能被poll到。

selectpoll一样,在大量客户端连接进来时,它们的效率会随着客户端数量而线性下降。

epoll:

Linux2.6开始支持的一个函数,是对selectpoll的增强版本。

优点:没有监视fd个数的限制,主动通知(回调)机制,只关注活跃的fd,不用像selectpoll那样全量遍历去找就绪的fd,因而也不存在随着客户端数量的增多而性能下降的问题。最后是内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少复制开销。

缺点:在大量客户端连接,并且大量活跃的fd时,其性能可能还不如select/poll

2.7:信号驱动式IO模型

信号驱动IO模型处理流程如下:

图5

通过上图,在信号驱动 IO 模型中,应用程序使用套接口进行信号驱动 IO,并安装一个信号处理函数,进程继续运行并不会发生阻塞;

当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 IO 操作函数处理数据。

这种模式下在大量IO操作时可能会发生信号队列溢出而导致无法通知。在TCP下,该模式几乎没用,TCP下可通知的条件过多,每一个都进行判断会消耗掉大量的资源。

2.8:异步IO模型(AIO)

上面介绍的几种IO模型,对于IO处理本身而言,都是同步的,只有这个模型,针对IO处理本身来讲,是异步的。

下面来看看流程图:

图6

由上图看出,此模型下首先由应用程序告知内核启动某个操作,并让内核在整个操作包括将数据从内核拷贝到应用程序的缓冲区的过程中,完成后通知应用程序。

这跟上面的信号驱动IO模型有所不同,这个模型通知给应用程序时,IO操作已经全部完成,应用程序直接拿数据就好,无需再做任何IO操作(这就是此模型叫异步IO处理的原因),而信号驱动通常是返回给应用程序一个数据报准备状态,真正的IO操作仍需要应用程序进行。

目前AIO并不完善,最常用的高性能IO模型仍然是IO多路复用模型。

三、总结

这几种模型除了AIO属于异步IO以外,其余的几种全都是同步IO(即需要应用程序主动进行IO操作),而是否阻塞应用程序取决于第一个阶段的处理方式,前几种IO模型的区别全在于第一阶段的处理。

本文参考:https://zhuanlan.zhihu.com/p/43933717