IO底层原理

该篇主要介绍从操作系统的角度来理解传统IO(Blocking IO)、NIO(Non-blocking IO)、AIO(Asynchronous IO)以及多路复用机制的底层原理。

I/O基础及传统IO

I/O基础概念

I/O(Input/Output)是计算机科学中用于描述数据在硬件设备之间或硬件与软件之间传输的概念。这包括了从键盘、鼠标等输入设备接收信息,以及将数据发送到显示器、打印机等输出设备。

输入/输出设备与数据传输

  • 输入设备:如键盘、鼠标、扫描仪,用于将外部信息转化为计算机可以处理的数据。
  • 输出设备:如显示器、打印机,用于将计算机处理后的数据以人可读的形式展现出来。
  • 数据传输:数据在设备间传输时,通常会经过一系列的转换,例如编码和解码,以适应不同的硬件接口。

DMA与CPU拷贝

在计算机系统中,数据的移动是一个常见的操作,特别是在需要在不同设备之间传输数据时,比如从磁盘读取数据到内存,或者从内存写数据到网络接口。数据的移动可以通过不同的方式进行,其中最常见的是DMA(Direct Memory Access)拷贝和CPU拷贝。

  • CPU拷贝:适合少量或快速的数据移动,但在大量数据传输时会显著降低CPU的可用性。
  • DMA拷贝:专为大量数据传输设计,由DMA控制器执行,释放了CPU去执行其他更重要的任务,提高了系统效率。

在现代计算机系统中,DMA技术被广泛应用于各种I/O操作,如磁盘读写、网络数据传输等,以提升数据传输的效率和系统整体的性能。

Blocking IO

传统IO,也称为阻塞IO(Blocking IO),是一种最基础的IO操作模式。在这种模式下,当一个进程或线程发起一个IO请求时,它将暂停并等待直到IO操作完成。这意味着在IO操作完成前,该线程将不会执行任何其他任务,处于阻塞状态。

在操作系统级别,一个阻塞IO操作通常包含以下步骤:

  1. 用户空间到内核空间的切换:应用程序通过系统调用(如readwrite)将控制权交给操作系统内核。
  2. 内核准备数据:内核开始读取或写入数据到磁盘或从磁盘读取数据。如果数据不在缓存中,这一步骤可能会花费较长时间。
  3. 数据复制到用户空间:数据准备好后,内核将数据从内核空间复制到应用程序的用户空间缓冲区。
  4. 控制返回用户空间:当数据完全复制完成后,内核将控制权返回给应用程序,此时应用程序的线程解除阻塞状态。

在Java中,java.io包提供了基于阻塞IO的API。详细的API及其使用参照:Java IO

虽然阻塞IO模型在处理少量的、不频繁的IO操作时较为简单直观,但在高并发场景下,其性能瓶颈和资源浪费问题变得明显,因此在现代高性能应用中,往往会选择使用非阻塞IO或其他异步IO模型来替代。

Java I/O体系

Java的I/O体系主要由两大核心组件构成:==流(Streams)和通道(Channels)==。它们分别对应着Java的传统I/O模型和NIO模型。

Java I/O包主要位于java.iojava.nio包中,其中:

  • java.io包含了传统的流式I/O操作,如InputStreamOutputStreamReaderWriter

  • java.nio引入了新的非阻塞I/O模型,包括ByteBufferFileChannelSelector等。

流和通道的区别主要在于流是面向连接的,而通道支持非阻塞操作,并且能够利用更底层的系统调用来提高性能。流适合于简单的I/O操作,而通道则更适合高并发、高性能的应用场景。

操作系统的IO支持

操作系统层面的I/O支持通过各种系统调用和API提供了丰富的功能,从基本的读写操作到高效的多路复用和异步I/O,这些机制对于开发高性能的网络和文件系统应用程序至关重要。了解这些底层细节有助于更有效地利用系统资源,优化应用程序的性能。

Linux/Unix

Syscalls(系统调用) 在Linux/Unix系统中,应用程序通过一系列的系统调用来与内核交互,执行I/O操作。以下是几种常见的系统调用:

  • read() 和 write():这是最基本的文件读写系统调用,它们是阻塞式的,意味着调用线程会一直等待直到读写操作完成。
  • select() 和 poll():这两种系统调用实现了多路复用,允许一个线程同时监控多个文件描述符的状态变化。当其中一个或多个描述符准备好读或写时,这些调用会返回。
  • epoll():epoll是select和poll的改进版,提供了更高的效率和更好的扩展性。epoll使用事件驱动的方式,只报告那些就绪的文件描述符,避免了轮询所有描述符的开销。
  • aio_read() 和 aio_write():这些系统调用支持异步I/O,允许应用程序在发起读写操作后立即返回,而不需要等待操作完成。操作系统会在I/O操作完成后通过回调函数或信号通知应用程序。

在Linux/Unix系统中,为了减少数据在用户空间和内核空间之间不必要的复制,从而提高I/O操作的效率。提供了几个常用的实现零拷贝的系统调用:

  1. mmap() mmap() 系统调用允许用户空间的进程直接访问内核缓冲区,也就是说,数据可以被直接映射到进程的地址空间,而不需要显式地从内核缓冲区复制到用户缓冲区。这在读取文件数据时特别有用,可以减少一次数据拷贝。

  2. sendfile() sendfile() 允许数据直接从一个文件描述符传输到另一个文件描述符,通常是将文件数据直接发送到网络套接字,而无需在用户空间和内核空间之间复制数据。这避免了两次数据拷贝,即从文件到用户缓冲区的拷贝和从用户缓冲区到网络栈的拷贝。

  3. splice() splice() 系统调用在Linux 2.6版本中引入,它可以在内核缓冲区和文件描述符之间直接传输数据,而不需要用户空间的介入。splice() 可以用于在两个文件描述符之间传输数据,也可以用于避免数据从内核缓冲区到用户空间再到另一个内核缓冲区的拷贝。

内核态与用户态

在现代操作系统的设计哲学中,内核态与用户态、内核空间与用户空间构成了计算机系统中至关重要的分界线,它们不仅是技术实现的基石,更是保障系统安全与稳定的守护神。

内核态与用户态、内核空间与用户空间的设定,体现了操作系统的深思熟虑。它们不仅是技术实现的必要条件,更是对系统安全与稳定性的深刻承诺。无论用户程序还是核心的系统服务,都能在一个既开放又安全的环境中和谐共存。

零拷贝思想

传统的IO方式中,以读取文件为例,需要经过一次硬盘到内核空间的拷贝,一次内核空间到用户空间的拷贝,Java的传统IO方式甚至还需要从用户空间拷贝到Java堆中。多次拷贝会导致整体效率大打折扣。

直接I/O、内存映射都是高效数据传输和处理的技术手段,都是零拷贝思想的体现,主要用于提高I/O操作的性能。

内存映射和直接I/O都是实现零拷贝的一种手段。通过减少数据在用户态和内核态之间的拷贝次数,实现更高的性能。

下面看一下使用缓存I/O从磁盘文件读取数据并发送到网络上的过程:

Linux在2.1版本中引入了 ==sendfile== 函数,可以实现将数据从一个文件描述符传输到另外一个文件描述符。减少了一次数据从内核缓冲区拷贝到用户缓冲区的过程,可以直接将内核缓冲区的数据拷贝到socket缓冲区。

Linux在2.4版本中引入了 ==gather== 技术,gather操作可以将内核缓冲区的内存地址、地址偏移量信息记录到socket缓冲区中,之后DMA根据地址信息从内存中读取数据到网卡中,减少了数据从内核缓冲区到socket缓冲区的拷贝过程。

Win32API函数

Windows操作系统同样具有内核态(Kernel Mode)和用户态(User Mode)的区分,同时也有自己的API集,用于处理I/O操作:

  • ReadFile() 和 WriteFile():类似于Linux中的read和write,但具有Windows特有的参数和行为。
  • WSAEventSelect() 和 WSAWaitForMultipleEvents():这些函数用于网络套接字的多路复用,类似于Linux中的select和poll。
  • CreateIoCompletionPort() 和 GetQueuedCompletionStatus():Windows使用IOCP(I/O Completion Ports)机制来支持高效的异步I/O操作。IOCP允许一个线程处理来自多个文件或套接字的I/O操作,而无需为每个操作创建单独的线程。

JavaIO的底层原理

BIO拷贝流程

在传统的Java I/O操作中,比如使用java.io.FileInputStreamjava.io.FileOutputStream,数据要经历多次拷贝才能从磁盘到达应用程序的堆内存中。

文件写入的逻辑与读取类似, 同样需要经历三次拷贝。

NIO核心原理

NIO,即Non-blocking IO,是一种优化过的IO处理方式,它克服了传统阻塞IO在高并发场景下的效率问题。NIO允许应用程序在等待IO操作时可以继续执行其他任务,提高了资源的利用效率和程序的响应速度。

在Java中,NIO的主要组件包括FileChannelDatagramChannelServerSocketChannel等,以及用于事件多路复用的SelectorSelectionKey

  • FileChannel:用于文件的非阻塞读写操作。
  • DatagramChannel:用于UDP数据报的非阻塞发送和接收。
  • Selector:核心组件之一,用于监听多个通道上的IO事件。
  • SelectionKey:表示一个通道在Selector上的注册状态,包括通道本身、感兴趣的事件集合以及已就绪的事件集合。

NIO的核心思想是基于==事件驱动和多路复用==。在NIO中,单个线程可以同时监听多个通道的IO操作状态,当某个通道就绪时(即有数据可读或可写),才进行实际的IO操作。


NIO还可以配合使用堆外内存减少一次IO过程中的拷贝,如:

java
RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 使用堆外内存
channel.read(buffer);

在使用Java NIO和直接ByteBuffer进行读取操作的过程中,数据经历了两次主要的拷贝:

  1. 磁盘到Page Cache:这是由操作系统处理的,与应用程序的直接控制无关。
  2. Page Cache到直接ByteBuffer:这是在用户空间和内核空间之间直接进行的,避免了Java堆上的额外复制。

文件映射和零拷贝

文件映射与传统IO流相比,少了从内核缓冲区将数据拷贝到用户缓冲区的步骤,减少了一次拷贝。

Java NIO中提供了MappedByteBuffer来处理文件映射,下面是一个读取文件的例子:

java
public class MappedByteBufferTest {
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile(new File("~l/test.txt"), "r")) {
            // 获取FileChannel
            FileChannel fileChannel = file.getChannel();
            long size = fileChannel.size();
            // 调用map方法进行文件映射,返回MappedByteBuffer
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
            byte[] bytes = new byte[(int)size];
            for (int i = 0; i < size; i++) {
                // 读取数据
                bytes[i] = mappedByteBuffer.get();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

零拷贝通常指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,减少数据的拷贝次数。

Java NIO中的FileChannel可以实现将数据从FileChannel直接传输到另一个Channel,它是sendfile的一种实现:

java
RandomAccessFile file = new RandomAccessFile(new File("~/test.txt"), "r");
// 获取FileChannel
FileChannel fileChannel = file.getChannel();
long size = fileChannel.size();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
fileChannel.transferTo(0,size,socketChannel);

AIO核心原理

AIO,即Asynchronous IO,是一种更先进的IO模型,它不仅非阻塞,而且异步。在AIO中,应用程序发起IO操作后可以立即返回去做其他事情,而不需要等待IO操作完成。当IO操作完成后,系统会通知应用程序,这样应用程序可以在合适的时间点去处理已完成的IO操作的结果。

Java 7引入了AIO的支持,主要通过java.nio.channels.AsynchronousFileChanneljava.nio.channels.AsynchronousSocketChannel这两个类来实现。

  • AsynchronousFileChannel:用于异步地读写文件。
  • AsynchronousSocketChannel:用于异步的网络通信。

总之,AIO提供了一种更加灵活和高效的IO处理方式,尤其适用于高并发的场景,可以显著提高应用程序的性能和响应速度。然而,AIO的实现和编程模型比NIO更复杂,需要开发者对异步编程有深入的理解和经验。

综合分析与应用

在不同的应用场景下,选择适当的I/O模型对于确保应用程序的性能和响应性至关重要。以下是三种主要I/O模型的选择依据:

  • Blocking IO:适合处理少量的并发连接,尤其是在I/O操作不是瓶颈的情况下。例如,单线程或低并发的应用可能使用阻塞I/O,因为它实现简单,易于理解和调试。

  • Non-blocking IO:适用于处理大量并发连接的场景,特别是在网络编程中。NIO通过多路复用器(如Selector)可以有效管理多个连接,减少线程数量,提高资源利用率。

  • Asynchronous IO:AIO是最先进的I/O模型,特别适合于I/O密集型应用,如大规模数据处理和高并发服务器。AIO的异步特性允许应用程序在等待I/O的同时执行其他任务,从而最大化CPU和I/O设备的利用率。

总之,I/O模型的选择应该基于具体的应用场景和性能需求。在设计网络服务器、大数据处理平台或任何I/O密集型应用时,深入理解各种I/O模型的原理和优势,能够帮助开发者做出最佳的技术决策,构建出既高效又可靠的系统。

未来的I/O技术将更加侧重于智能化、自动化和高性能。随着AI和机器学习算法的进步,智能调度和预测性维护将成为I/O管理的重要组成部分。此外,边缘计算的兴起也将推动I/O技术向更低延迟和更高带宽的方向发展,以支持实时数据分析和处理。最终,I/O技术的目标是无缝集成到云原生生态系统中,提供无缝的用户体验和卓越的服务质量,同时降低总体拥有成本(TCO)。


Java中的IO及零拷贝:https://www.cnblogs.com/shanml/p/16756395.html