Java NIO

Java NIO引入了一套新的抽象用于I/O处理。关于NIO首先要理解以下几点:

  • 缓冲区的操作;
  • 内核空间和用户进程空间;
  • 虚拟内存和分页;
  • 面向流的I/O和面向文件的I/O;
  • 多工I/O(就绪性选择);


缓冲区操作

 

缓冲区和缓冲区如何操作,是所有I/O的基础。所谓输入和输出无非就是讲数据移入和移出缓冲区。

进程执行 I/O 操作,归结起来,也就是向操作系统发出请求,让它要么把缓冲区里的数据写出,要么把数据读入缓冲区。

当进程发起read系统调用,要求其缓冲区被填满。此时内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核的内存缓冲区中,这一步通过DMA完成,无需主 CPU 协助。一旦磁盘控制器把缓冲区填满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行read调用时指定的缓冲区中去。其示意图如下:


注意用户空间是常规进程所在区域,JVM就是常规进程,驻守于用户空间。用户空间是非特权区域,在该区域执行的代码不能直接访问硬件设备。内核空间是操作系统所在区域。内核代码能与设备控制器通讯,控制着用户区域进程的运行状态等,最重要的是,所有I/O都直接或间接通过内核空间。

当进程请求I/O操作时,它执行一个系统调用(有时称为陷阱)并将控制权交给内核。当内核以这种方式被调用,它随即采取必要的步骤找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。此时该数据只需简单地拷贝出来即可。否则进程将被挂起,内核再把数据读进内存。

此时为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,充当中间人的角色。

发散/汇聚


许多操作系统能把组装/分解过程进行得更加高效。根据发散/汇聚的概念,进程只需一个系统调用,就能把一连串缓冲区地址传递给操作系统。然后,内核就可以顺序填充或排干多个缓冲区,读的时候就把数据发散到多个用户空间缓冲区,写的时候再从多个缓冲区把数据汇聚起来。

三个缓冲区的发散读操作:


这样用户进程就不必多次执行系统调用,内核也可以优化数据的处理过程,因为它已掌握待传输数据的全部信息。如果系统配有多个CPU,甚至可以同时填充或排干多个缓冲区。


虚拟内存


几乎所有的现代操作系统都使用虚拟内存。虚拟内存是指通过使用虚拟地址来取代物理(硬件RAM)内存地址的技术。这样做好处颇多,总结起来可分为两大类:
  1. 一个以上的虚拟地址可指向同一个物理内存地址。
  2. 虚拟内存空间可大于实际可用的硬件内存。
设备控制器不能通过DMA直接访问用户空间,但通过把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,则可以达到相同效果。这样DMA硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区。

内存空间的多重映射:


这样省去了内核与用户空间之间的拷贝。但前提条件是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小(通常为 512 字节磁盘扇区)的倍数。操作系统把内存地址空间划分为页,即固定大小的字节组。内存页的大小总是磁盘块大小的倍数,通常为 2 次幂(这样可简化寻址操作)。典型的内存页为 1,024、2,048 和 4,096 字节。虚拟和物理内存页的大小总是相同的。

下图显示了来自多个虚拟地址的虚拟内存页是如何映射到物理内存的:

为了支持虚拟内存的寻址空间大于物理内存,就必须进行虚拟内存分页(或称为交换,真正的交换是在进程层面完成而非页层面)。即虚拟内存空间的页面能够继续存在于外部磁盘存储,这样就为物理内存中的其他虚拟页面腾出了空间。从本质上说,物理内存充当了分页区的高速缓存;而所谓分页区,即从物理内存置换出来,转而存储于磁盘上的内存页面。其示意图如下:


把内存页大小设定为磁盘块大小的倍数,这样内核就可直接向磁盘控制硬件发布命令,把内存页写入磁盘,在需要时再重新装入。结果是,所有磁盘 I/O 都在页层面完成。对于采用分页技术的现代操作系统而言,这也是数据在磁盘与物理内存之间往来的唯一方式。

现代 CPU 包含一个称为内存管理单元(MMU)的子系统,逻辑上位于 CPU 与物理内存之间。该设备包含虚拟地址向物理内存地址转换时所需映射信息。当 CPU 引用某内存地址时,MMU负责确定该地址所在页(往往通过对地址值进行移位或屏蔽位操作实现),并将虚拟页号转换为物理页号(这一步由硬件完成,速度极快)。如果当前不存在与该虚拟页形成有效映射的物理内存页,MMU 会向 CPU 提交一个页错误。

页错误随即产生一个陷阱(类似于系统调用),把控制权移交给内核,附带导致错误的虚拟地址信息,然后内核采取步骤验证页的有效性。内核会安排页面调入操作,把缺失的页内容读回物理内存。这往往导致别的页被移出物理内存,好给新来的页让地方。在这种情况下,如果待移出的页已经被碰过了(自创建或上次页面调入以来,内容已发生改变),还必须首先执行页面调出,把页内容拷贝到磁盘上的分页区。如果所要求的地址不是有效的虚拟内存地址(不属于正在执行的进程的任何一个内存段),则该页不能通过验证,段错误随即产生。于是,控制权转交给内核的另一部分,通常导致的结果就是进程被强令关闭。

一旦出错的页通过了验证,MMU 随即更新,建立新的虚拟到物理的映射(如有必要,中断被移出页的映射),用户进程得以继续。造成页错误的用户进程对此不会有丝毫察觉,一切都在不知不觉中进行。


文件I/O


文件 I/O 属文件系统范畴,文件系统与磁盘迥然不同。磁盘把数据存在扇区上,通常一个扇区512字节。磁盘属硬件设备,对何谓文件一无所知,它只是提供了一系列数据存取窗口。在这点上,磁盘扇区与内存页颇有相似之处:都是统一大小,都可作为大的数组被访问。文件系统是更高层次的抽象,是安排、解释磁盘(或其他随机存取块设备)数据的一种独特方式。您所写代码几乎无一例外地要与文件系统打交道,而不是直接与磁盘打交道。是文件系统定义了文件名、路径、文件、文件属性等抽象概念。

所有I/O都是通过请求页面调度完成的。您应该还记得,页面调度是非常底层的操作,仅发生于磁盘扇区与内存页之间的直接传输。而文件 I/O 则可以任意大小、任意定位。那么,底层的页面调度是如何转换为文件 I/O 的?

文件系统把一连串大小一致的数据块组织到一起。有些块存储元信息,如空闲块、目录、索引等的映射,有些包含文件数据。单个文件的元信息描述了哪些块包含文件数据、数据在哪里结束、最后一次更新是什么时候,等等。

当用户进程请求读取文件数据时,文件系统需要确定数据具体在磁盘什么位置,然后着手把相关磁盘扇区读进内存。老式的操作系统往往直接向磁盘驱动器发布命令,要求其读取所需磁盘扇区。而采用分页技术的现代操作系统则利用请求页面调度取得所需数据。操作系统还有个页的概念,其大小或者与基本内存页一致,或者是其倍数。典型的操作系统页从 2,048 到 8,192 字节不等,且始终是基本内存页大小的倍数。


内存映射文件


传统的文件 I/O 是通过用户进程发布 read( )和 write( )系统调用来传输数据的。为了在内核空间的文件系统页与用户空间的内存区之间移动数据,一次以上的拷贝操作几乎总是免不了的。这是因为,在文件系统页与用户缓冲区之间往往没有一一对应关系。但是,还有一种大多数操作系统都支持的特殊类型的 I/O 操作,允许用户进程最大限度地利用面向页的系统 I/O 特性,并完全摒弃缓冲区拷贝。这就是内存映射 I/O,如下图所示:

内存映射 I/O 使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。这样做有几个好处:
  • 用户进程把文件数据当作内存,所以无需调用read或write系统调用。
  • 当用户进程碰触到映射内存空间,页错误会自动产生,从而将文件数据从磁盘读进内存。如果用户修改了映射内存空间,相关页会自动标记为脏,随后刷新到磁盘,文件得到更新。
  • 操作系统的虚拟内存子系统会对页进行智能高速缓存,自动根据系统负载进行内存管理。
  • 数据总是按页对齐的,无需执行缓冲区拷贝。
  • 大型文件使用映射,无需耗费大量内存,即可进行数据拷贝。
在处理大量数据时,尤其要记得这一点。如果数据缓冲区是按页对齐的,且大小是内建页大小的倍数,那么,对大多数操作系统而言,其处理效率会大幅提升。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值