Java IO内部是如何工作的?

本文旨在为那些好奇知道如何在机器级别映射Java IO操作以及在应用程序运行时硬件始终做什么的读者编写。

我假设您熟悉基本的IO操作,例如读取文件、通过JavaIO API编写文件;因为这超出了本文的范围。

缓冲区处理和内核与用户空间

“input/output”一词只意味着将数据移出缓冲区。

缓冲区以及缓冲区的处理方式是所有IO的基础。你只要时刻记住这个。

通常,进程通过请求操作系统从缓冲区(写入操作)中排出数据或用数据填充缓冲区(读取操作)来执行IO。这是IO概念的全部总结。

操作系统中执行这些传输的机器可能非常复杂,但从概念上讲,它非常简单,我们将在本文中讨论其中的一小部分。

os级的数据缓冲

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

首先,进程请求通过调用read()系统来填充其缓冲区。

读调用导致内核向磁盘控制器硬件发出命令,以从磁盘中获取数据。

磁盘控制器通过DMA直接将数据写入内核内存缓冲区,而不需要主CPU的进一步帮助。

磁盘控制器完成缓冲区填充后,内核将数据从内核空间中的临时缓冲区复制到进程指定的缓冲区;当请求read()操作时。

需要注意的一点是内核试图缓存和/或预取数据,因此进程请求的数据可能已经在内核空间中可用。如果是,则将复制流程请求的数据。

如果数据不可用,则进程将暂停,而内核则将数据带入内存。

虚拟内存

您一定已经多次听说过虚拟内存。让我想想。

所有现代操作系统都利用虚拟内存。虚拟内存是指人工地址或虚拟地址被用来代替物理(硬件RAM)内存地址。

虚拟内存带来两个重要优势:

多个虚拟地址可以引用相同的物理内存位置。

虚拟内存空间可以大于实际可用硬件内存。

在前面的部分中,从内核空间复制到最终用户缓冲区似乎是额外的工作。为什么不告诉磁盘控制器直接发送到userspace中的缓冲区?好吧,它是用虚拟内存完成的,这是上面的优势1。

通过将内核空间地址映射到与用户空间中的虚拟地址相同的物理地址,DMA硬件(只能访问物理内存地址)可以填充内核和用户空间进程同时可见的缓冲区。

这消除了内核和用户空间之间的拷贝,但需要内核和用户缓冲区共享相同的页面对齐方式。缓冲区还必须是磁盘控制器使用的块大小的倍数(通常是512字节磁盘扇区)。

操作系统将内存地址空间划分为页,页是固定大小的字节组。这些内存页总是磁盘块大小的倍数,通常是2的幂(这简化了寻址)。典型的内存页大小是1024、2048和4096字节。

虚拟内存页和物理内存页的大小总是相同的。

内存分页

为了支持虚拟内存的第二个优点(可寻址空间大于物理内存),有必要进行虚拟内存分页(通常称为交换)。

内存分页是一种方案,通过这种方案,虚拟内存空间的页面可以持久化到外部磁盘存储器中,以便在物理内存中为其他虚拟页面腾出空间。从本质上讲,物理内存充当分页区域的缓存,分页区域是磁盘上的空间,当物理内存被挤出时,存储内存页的内容。

将内存页大小调整为磁盘块大小的倍数允许内核向磁盘控制器硬件发出直接命令,将内存页写入磁盘或在需要时重新加载它们。

原来,所有磁盘IO都是在页面级别完成的。在现代的分页操作系统中,这是数据在磁盘和物理内存之间移动的唯一方式。

现代CPU包含一个子系统,称为内存管理单元(MMU)。这个设备在逻辑上位于CPU和物理内存之间。MMU包含将虚拟地址转换为物理内存地址所需的映射信息。

当CPU引用内存位置时,MMU确定该位置所在的页(通常通过移动或屏蔽地址值的位)并将该虚拟页号转换为物理页号(这在硬件中完成,并且非常快)。

面向文件/块的IO

文件IO总是发生在文件系统的上下文中。文件系统与磁盘截然不同。磁盘将数据存储在扇区中,每个扇区通常为512字节。它们是对文件的语义一无所知的硬件设备。它们只是提供了一些可以存储数据的插槽。在这方面,磁盘的扇区类似于内存页;所有扇区的大小都是一致的,并且可以作为一个大数组寻址。

另一方面,文件系统是更高层次的抽象。文件系统是一种特殊的方法,用于排列和解释存储在磁盘(或其他一些随机存取、面向块的设备)上的数据。您编写的代码几乎总是与文件系统交互,而不是直接与磁盘交互。文件系统定义了文件名、路径、文件、文件属性等的抽象。

文件系统(在硬盘中)组织一系列大小一致的数据块。一些块存储元信息,如空闲块、目录、索引等的映射。其他块包含实际的文件数据。

有关单个文件的元信息描述了哪些块包含文件数据、数据结束的位置、上次更新的时间等。

当用户进程请求读取文件数据时,文件系统实现会准确地确定数据在磁盘上的位置。然后它采取措施将这些磁盘扇区放入内存。

文件系统也有页的概念,页的大小可以是基本内存页的大小,也可以是基本内存页的倍数。典型的文件系统页面大小从2048字节到8192字节不等,并且总是基本内存页面大小的倍数。

分页文件系统如何执行IO归结为以下逻辑步骤:

确定请求跨越的文件系统页(磁盘扇区组)。磁盘上的文件内容和/或元数据可能分布在多个文件系统页面上,这些页面可能是不连续的。

在内核空间中分配足够的内存页以容纳已标识的文件系统页。

在这些内存页和磁盘上的文件系统页之间建立映射。

为每个内存页生成页错误。

虚拟内存系统捕获页面错误并调度pagein(即分页空间pagein),通过从磁盘读取页面内容来验证这些页面。

一旦pagein完成,文件系统就会分解原始数据以提取请求的文件内容或属性信息。

注意,这个文件系统数据将像其他内存页一样被缓存。在随后的IO请求中,部分或全部文件数据可能仍存在于物理内存中,并且可以重用,而无需从磁盘重新读取

文件锁定

文件锁定是一种方案,通过该方案,一个进程可以阻止其他进程访问某个文件或限制其他进程访问该文件的方式。虽然“文件锁定”这个名称意味着锁定整个文件(通常是这样做的),但锁定通常在更细粒度的级别上可用。

文件区域通常是锁定的,粒度降低到字节级别。锁与特定文件相关联,从该文件中的特定字节位置开始,并在特定的字节范围内运行。这一点很重要,因为它允许许多进程协调对文件特定区域的访问,而不会妨碍文件中其他进程的工作。

文件锁有两种类型:共享锁和独占锁。多个共享锁可能同时对同一文件区域有效。另一方面,独占锁要求没有其他锁对请求的区域有效。

流IO

并非所有IO都是面向块的。还有流IO,它是基于管道建模的。必须按顺序访问IO流的字节。TTY(控制台)设备、打印机端口和网络连接是流的常见示例。

流通常(但不一定)比块设备慢,并且通常是间歇输入的源。大多数操作系统允许将流置于非阻塞模式,这允许进程检查流上的输入是否可用,如果此时没有可用的输入,则不会卡住。这样的功能允许进程在输入到达时处理输入,但在输入流空闲时执行其他功能。

超越非阻塞模式的一个步骤是能够进行就绪选择。这类似于非阻塞模式(并且通常构建在非阻塞模式之上),但是减轻了对流是否准备好用于操作系统的检查。

可以告诉操作系统监视流的集合,并向进程返回指示哪些流已就绪。这种能力允许进程利用操作系统返回的就绪信息,使用公共代码和单个线程多路复用多个活动流。

流IO在网络服务器中被广泛用于处理大量的网络连接。准备就绪选择对于高容量扩展至关重要。

这些都是关于这个有大量技术词汇的非常复杂的话题🙂