乐趣区

关于java:高并发IO的底层原理及4种主要IO模型

Java NIO 系列文章


  1. 高并发 IO 的底层原理及 4 种次要 IO 模型
  2. Buffer 的 4 个属性及重要办法

IO 读写的原理


大家晓得,用户程序进行 IO 读写,依赖于操作系统底层的 IO 读写,基本上会用到底层的 read&write 两大零碎调用。

这里波及一个根底的常识:

read 零碎调用,并不是间接从物理设施把数据读取到内存中,write 零碎调用,也不是把数据间接写入到物理设施

下层利用无论是调用操作系统的 read,还是调用操作系统的 write,都会波及缓冲区。具体来说,调用操作系统的 read,是把数据从内核缓冲区复制到过程缓冲区;而 write 零碎调用,是把数据从过程缓冲区复制到内核缓冲区。

上图显示了块数据如何从内部源(例如硬盘)挪动到正在运行的过程(例如 RAM)外部的存储区的简化“逻辑”图。

  • 首先,该过程通过进行 read() 零碎调用来填充其缓冲区。
  • read 读取调用会导致内核向磁盘控制器硬件收回命令以从磁盘获取数据。
  • 磁盘控制器通过 DMA 将数据间接写入内核内存缓冲区。
  • 磁盘控制器实现缓冲区的填充后,内核将数据从内核空间中的长期缓冲区复制到过程指定的缓冲区中。

为什么设置这么多缓冲区呢?

缓冲区的目标,是为了缩小频繁地与设施之间的物理替换。大家都晓得,外部设备的间接读写,波及操作系统的中断。产生零碎中断时,须要保留之前的过程数据和状态等信息,而完结中断之后,还须要复原之前的过程数据和状态等信息。为了缩小这种底层零碎的工夫损耗、性能损耗,于是呈现了内存缓冲区。

有了内存缓冲区,下层利用应用 read 零碎调用时,仅仅把数据从内核缓冲区复制到下层利用的缓冲区(过程缓冲区);下层利用应用 write 零碎调用时,仅仅把数据从过程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,期待缓冲区达到肯定数量的时候,再进行 IO 设施的中断解决,集中执行物理设施的理论 IO 操作,这种机制晋升了零碎的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序则不须要关怀

从数量上来说,在 Linux 零碎中,操作系统内核只有一个内核缓冲区。而每个用户程序(过程),有本人独立的缓冲区,叫作过程缓冲区。所以,用户程序的 IO 读写程序,在大多数状况下,并没有进行理论的 IO 操作,而是在过程缓冲区和内核缓冲区之间间接进行数据的替换

文件描述符

文件句柄,也叫文件描述符。在 Linux 零碎中,文件可分为:一般文件、目录文件、链接文件和设施文件。文件描述符(File Descriptor)是内核为了高效治理已被关上的文件所创立的索引,它是一个非负整数(通常是小整数),用于指代被关上的文件。所有的 IO 零碎调用,包含 socket 的读写调用,都是通过文件描述符实现的。

4 种次要的 IO 模型


介绍 4 种 IO 模型之前要先介绍两组概念

阻塞与非阻塞

阻塞 IO,指的是须要内核 IO 操作彻底实现后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态。传统的 IO 模型都是同步阻塞 IO。在 Java 中,默认创立的 socket 都是阻塞的

同步与异步

同步 IO,是一种用户空间与内核空间的 IO 发动形式。同步 IO 是指用户空间的线程是被动发动 IO 申请的一方,内核空间是被动接受方。异步 IO 则反过来,是指零碎内核是被动发动 IO 申请的一方,用户空间的线程是被动接受方

同步阻塞 IO(Blocking IO)

在 Java 应用程序过程中,默认状况下,所有的 socket 连贯的 IO 操作都是同步阻塞 IO(Blocking IO)。

在阻塞式 IO 模型中,Java 应用程序从 IO 零碎调用开始,直到零碎调用返回,在这段时间内,Java 过程是阻塞的。返回胜利后,利用过程开始解决用户空间的缓存区数据。

  1. 从 Java 启动 IO 读的 read 零碎调用开始,用户线程就进入阻塞状态。
  2. 当零碎内核收到 read 零碎调用,就开始筹备数据。一开始,数据可能还没有达到内核缓冲区(例如,还没有收到一个残缺的 socket 数据包),这个时候内核就要期待。
  3. 内核始终等到残缺的数据达到,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),而后内核返回后果(例如返回复制到用户缓冲区中的字节数)。
  4. 直到内核返回后,用户线程才会解除阻塞的状态,从新运行起来。

阻塞 IO 的长处是:

利用的程序开发非常简单;在阻塞期待数据期间,用户线程挂起。在阻塞期间,用户线程根本不会占用 CPU 资源。

阻塞 IO 的毛病是:

个别状况下,会为每个连贯装备一个独立的线程;反过来说,就是一个线程保护一个连贯的 IO 操作。在并发量小的状况下,这样做没有什么问题。然而,当在高并发的利用场景下,须要大量的线程来保护大量的网络连接,内存、线程切换开销会十分微小。因而,基本上阻塞 IO 模型在高并发利用场景下是不可用的。

同步非阻塞 NIO(None Blocking IO)

  1. 在内核数据没有筹备好的阶段,用户线程发动 IO 申请时,立刻返回。所以,为了读取到最终的数据,用户线程须要一直地发动 IO 零碎调用。
  2. 内核数据达到后,用户线程发动零碎调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),而后内核返回后果(例如返回复制到的用户缓冲区的字节数)。
  3. 用户线程读到数据后,才会解除阻塞状态,从新运行起来。也就是说,用户过程须要通过屡次的尝试,能力保障最终真正读到数据,而后继续执行。

同步非阻塞 IO 的特点:

应用程序的线程须要一直地进行 IO 零碎调用,轮询数据是否曾经筹备好,如果没有筹备好,就持续轮询,直到实现 IO 零碎调用为止。

同步非阻塞 IO 的长处:

每次发动的 IO 零碎调用,在内核期待数据过程中能够立刻返回。用户线程不会阻塞,实时性较好。

同步非阻塞 IO 的毛病:

一直地轮询内核,这将占用大量的 CPU 工夫,效率低下

总体来说,在高并发利用场景下,同步非阻塞 IO 也是不可用的。个别 Web 服务器不应用这种 IO 模型。这种 IO 模型个别很少间接应用,而是在其余 IO 模型中应用非阻塞 IO 这一个性。在 Java 的理论开发中,也不会波及这种 IO 模型

IO 多路复用模型(IO Multiplexing)

如何防止同步非阻塞 IO 模型中轮询期待的问题呢?这就是 IO 多路复用模型

在 IO 多路复用模型中,引入了一种新的零碎调用,查问 IO 的就绪状态。在 Linux 零碎中,对应的零碎调用为 select/epoll 零碎调用。通过该零碎调用,一个过程能够监督多个文件描述符,一旦某个描述符就绪(个别是内核缓冲区可读 / 可写),内核可能将就绪的状态返回给应用程序。随后,应用程序依据就绪的状态,进行相应的 IO 零碎调用。

目前反对 IO 多路复用的零碎调用,有 select、epoll 等等。select 零碎调用,简直在所有的操作系统上都有反对,具备良好的跨平台个性。epoll 是在 Linux 2.6 内核中提出的,是 select 零碎调用的 Linux 加强版本。

在 IO 多路复用模型中通过 select/epoll 零碎调用,单个应用程序的线程,能够一直地轮询成千盈百的 socket 连贯,当某个或者某些 socket 网络连接有 IO 就绪的状态,就返回对应的能够执行的读写操作

举个例子来阐明 IO 多路复用模型的流程。发动一个多路复用 IO 的 read 读操作的零碎调用,流程如下:1. 选择器注册。在这种模式中,首先,将须要 read 操作的指标 socket 网络连接,提前注册到 select/epoll 选择器中,Java 中对应的选择器类是 Selector 类。而后,才能够开启整个 IO 多路复用模型的轮询流程。

  1. 就绪状态的轮询。通过选择器的查询方法,查问注册过的所有 socket 连贯的就绪状态。通过查问的零碎调用,内核会返回一个就绪的 socket 列表。当任何一个注册过的 socket 中的数据筹备好了,内核缓冲区有数据(就绪)了,内核就将该 socket 退出到就绪的列表中。当用户过程调用了 select 查询方法,那么整个线程会被阻塞掉。
  2. 用户线程取得了就绪状态的列表后,依据其中的 socket 连贯,发动 read 零碎调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
  3. 复制实现后,内核返回后果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

IO 多路复用模型的特点

  1. 波及两种零碎调用(System Call),
  2. 一种是 select/epoll(就绪查问)
  3. 一种是 IO 操作。
  4. 和 NIO 模型类似,多路复用 IO 也须要轮询。负责 select/epoll 状态查问调用的线程,须要一直地进行 select/epoll 轮询,查找出达到 IO 操作就绪的 socket 连贯。

IO 多路复用模型的长处

与一个线程保护一个连贯的阻塞 IO 模式相比,应用 select/epoll 的最大劣势在于,一个选择器查问线程能够同时解决成千上万个连贯(Connection)。零碎不用创立大量的线程,也不用保护这些线程,从而大大减小了零碎的开销。

IO 多路复用模型的毛病

实质上,select/epoll 零碎调用是阻塞式的,属于同步 IO。都须要在读写事件就绪后,由零碎调用自身负责进行读写,也就是说这个读写过程是阻塞的

如果要彻底地解除线程的阻塞,就必须应用异步 IO 模型

异步 IO 模型(Asynchronous IO)

异步 IO 模型(Asynchronous IO,简称为 AIO)。AIO 的根本流程是:用户线程通过零碎调用,向内核注册某个 IO 操作。内核在整个 IO 操作(包含数据筹备、数据复制)实现后,告诉用户程序,用户执行后续的业务操作。

举个例子。发动一个异步 IO 的 read 读操作的零碎调用,流程如下:1. 当用户线程发动了 read 零碎调用,立即就能够开始去做其余的事,用户线程不阻塞。2. 内核就开始了 IO 的第一个阶段:筹备数据。等到数据筹备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)。3. 内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,通知用户线程 read 操作实现了。4. 用户线程读取用户缓冲区的数据,实现后续的业务操作。

异步 IO 模型的特点

在内核期待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程须要接管内核的 IO 操作实现的事件,或者用户线程须要注册一个 IO 操作实现的回调函数。正因为如此,异步 IO 有的时候也被称为信号驱动 IO

异步 IO 异步模型的毛病

应用程序仅须要进行事件的注册与接管,其余的工作都留给了操作系统,也就是说,须要底层内核提供反对。实践上来说,异步 IO 是真正的异步输入输出,它的吞吐量高于 IO 多路复用模型的吞吐量

就目前而言,Windows 零碎下通过 IOCP 实现了真正的异步 IO。而在 Linux 零碎下,异步 IO 模型在 2.6 版本才引入,目前并不欠缺,其底层实现仍应用 epoll,与 IO 多路复用雷同,因而在性能上没有显著的劣势。大多数的高并发服务器端的程序,个别都是基于 Linux 零碎的。因此,目前这类高并发网络应用程序的开发,大多采纳 IO 多路复用模型

总结


退出移动版