Java NIO - 简介

来源:互联网 发布:你好旧时光 知乎 编辑:程序博客网 时间:2024/05/16 09:19
I/O的时间开销要比内存内的任务处理时间开销来的大得多,所以再优美的代码也可能会因为低效的I/O而给调优带来甚少的回报。过去由于JVM欠缺编译优化,大家主要的关注点还没有落在I/O处理上,而现在,随着JVM的发展,字节码的运行已经接近甚至某些场合超过了本地代码的运行速度。此时I/O的瓶颈开始凸显出来。但是I/O的瓶颈又并非是操作系统造成的,而是Java的流式处理,将操作系统运送过来的大块数据(字节数组)以小片(单个字节、单行文本)的方式进行切分消化。

NIO提供了一系列的I/O比喻,拉近了抽象与现实间的差距。

  • 处理Buffer

概念上,I/O的核心就是将数据写入Buffer供使用(读)和从Buffer取出写入外部资源(写)。

  • 内核与用户空间



上图展示了一个读操作的过程。进程发出read系统调用,要求内核填充自己的buffer(用户空间)。如此时,请求的数据未被缓存在内核空间中(内核会尝试缓存或提前获取数据),内核向磁盘控制器发出读请求,随后,磁盘控制器通过DMA将请求的数据写入内核指定的buffer(内核空间)。当写入完成后,内核将自己buffer的数据拷贝进进程指定的buffer内。

用户空间是一个非私有的内存空间,JVM等常规进程就运行于此空间内,这个空间里的进程不能直接访问硬件设备。内核空间是操作系统运行着的内存空间,它有特殊的权限,可以直接与设备控制器打交道,还能操作用户空间内进程的状态等。所有的IO操作都是直接或者间接的通过内核空间的。

尽管拷贝这个动作看上去有些多余,但是是必须的。首先,设备通常没有权限直接访问用户空间。其次,基于块的设备(例如磁盘),通常工作在挪动固定大小的块上,而这个大小可能和用户进程请求的大小是不匹配的,此时,内核就做了这么一个分割拼装的中间人角色。



scatter/gather的概念就是允许进程在读写操作的时候提供多个buffer,此时内核会顺序的针对每一个buffer进行处理。这样就能节省多次系统调用带来的昂贵开销,同时内核可以针对多个buffer进行优化处理。甚至,多CPU的情况下,多个buffer能够并行执行。

  • 虚拟内存

虚拟内存就是利用人工的,虚拟的内存地址来代替真正的物理地址被实际的进程所使用。这会带来多个好处。一来,多个虚拟地址可以映射到一个真正的物理地址上,再者,虚拟的地址空间可以大于实际的物理内存地址空间。



利用虚拟内存就可以解决上面所说的重复拷贝问题,将内核空间的buffer地址映射到虚拟地址上供进程直接访问(共享内存页)。

  • 分页



操作系统会将内存地址分为固定大小(字节)的多组,这一组就称为一个内存页。内存页的大小通常是磁盘块大小的倍数且是2的N次,典型的大小为1024、2048和4096字节。虚拟内存和物理内存的内存页大小总是相同的。

虚拟内存空间大于实际物理内存空间的机制是由页交换(真正的交换是在进程级别的)做到的。当物理内存满,而又需要进入新的数据,这时,就会将当前不用的内存页交换出,持久到外部磁盘,将空出来的内存页空间放新数据。本质上,物理内存就是内存页的缓存,而外部磁盘才是内存页实际存储的地方。



上图为5条进程和各自的内存页驻留情况。

将内存页大小对齐为磁盘块大小的倍数(内存页刚好装入N个磁盘块,一个磁盘块不会被分割在多个内存页中。)是有好处的,它允许内核发送指令给磁盘控制器直接读写内存页到磁盘。这也指出了,在现代分页的操作系统中,所有的磁盘IO都是直接与内存页交互的。

现代的CPU包含一个子系统MMU(内存管理单元),它逻辑上坐落在物理内存和CPU之间,虚拟内存的功能就是由它完成的,它会转化虚拟地址到真实的物理地址。

首先MMU会根据虚拟地址找到地址所处的虚拟内存页,再找到对应的物理内存页(都是由硬件完成,相当迅速),如果找不到,会抛出页错误(page fault)给CPU。接下去,页错误会被内核捕获,内核会采取一系列步骤验证并从外部磁盘找到且交换进丢失的页入物理内存。伴随着的,可能会有其他的页被交换出物理内存,在这种情况下,如果这个出去的页是被修改过的,必须先同步其与外部磁盘上的数据。

如果在上步中,验证失败,即内核无法根据虚拟内存地址找到对应的物理资源,则段错误(segmentation fault)将会被抛出。通常这种情况下,进程就会被杀死了。

一旦验证成功,内存页被调入物理内存,MMU将会更新建立起这个虚拟物理间的映射(如有必要,可切断交换出页的映射。),此时,用户进程就可以继续执行下去了。而这一切对于用户进程来说都是透明的、无感知的。

这个动态的按需切换过程称作按需调页(demand paging),其中利用了很多成熟的算法来优化处理和防止失效。

  • 基于文件与I/O流

File I/O是基于文件系统的。物理磁盘对文件这种形式其实是无感的,它只关心数据块,所有的数据都存储在事先划分好的数据块中,统一的大小,具备可寻找的地址。文件系统是一层高层的抽象,它具有一系列的方法来定义,安排和解释磁盘上的数据。文件名、文件、路径和文件属性等,都是被文件系统抽象定义出来的。我们的代码总是在与文件系统打交道。

在磁盘上,有一部分数据块存储的是元数据,例如一个文件的内容被存放在哪几个数据块中、文件的属性等,而另一部分存储的才是文件内容数据。当一个读请求来到磁盘控制器,文件系统将会根据文件信息在元数据中找到对应的文件内容数据块,将他们载入内存中。

在文件系统中也有页的概念,一个页的大小是内存页大小的一到多倍。

在文件系统中I/O的操作可以细化为:

  1. 查看请求的文件涉及多少个文件系统页,这些文件的内容或元数据信息可能分布在多个文件系统页中。
  2. 在内核空间中分配足够内存页空间来加载这些文件系统页。
  3. 建立起内存页和文件系统页间的映射
  4. 为每一个内存页生成一个页错误(page fault)
  5. 虚拟内存系统捕获页错误,开始进行验证。
  6. 一旦数据已经被交换入物理内存,文件系统就开始分割原始数据(读取文件内容或文件信息,由于文件系统页大于内存页。)。

这些被载入内存的文件数据会被缓存起来,当接下去再有I/O请求的数据在其中可以读到的时候,将不再此从磁盘加载。

许多文件系统会假设进程将会继续读文件的剩余部分而将剩余涉及的文件系统页提前载入内存中,这些额外的页可能会在内存中保持有效状态相当一段时间。当下次再读取文件的时候,将会直接读,而不用再次打开文件。

对于写操作也是同样类似的步骤。当文件在内存中被修改时,该内存页在交换出的时候将会先同步其在外部磁盘上得数据。新创建的文件将会建立起与空白文件系统页的映射,在写操作之后会被刷新入文件系统。



由于用户buffer和文件系统页之间通常不是一一对应的,所以,传统意义上得I/O总是无法避免一次或多次的用户空间与内核空间的拷贝。但是,内存映射I/O可以。

内存映射I/O利用文件系统建立起与虚拟内存得映射,使得用户控件可以直接访问到文件系统页。这样做好处多多:

  1. 用户进程是以内存访问的方式看到文件,所以避免了读或写得系统调用。
  2. 用户进程尝试读取文件的时候会触发页错误,从而将文件数据从磁盘带入内存。如果用户对内存中得文件数据做了改动,内存页将会被标记为脏,之后这些改动数据将会被同步回磁盘上的文件。
  3. 虚拟内存会根据系统负载灵活的按需调页。
  4. 数据永远都是对齐页大小的,没有buffer拷贝。
  5. 非常大得文件可以被映射进来而同时又不会占用大量的内存来拷贝数据。

虚拟内存和磁盘I/O是紧密关联着的,很多时候,它们是同一个东西的两个面。

文件锁,是一种模式,用来约束别的进程对同一个资源文件的访问。经管从名字上看文件锁似乎是锁了整个文件,但是事实上,锁可以到达字节粒度,不同的进程可以在同一个文件的不同区域进行各自的活动。

文件锁分为两类:共享的(shared)和排斥的(exclusive)。多个共享锁可以在同一时间对同一块区域起作用,而另一方面,排斥锁,要求没有其他的锁作用于请求的区域。两种锁典型的应用场景是控制对一个共享读的文件进行写操作。当一个进程对一个文件(整体或部分)进行读访问时会加上一个共享锁,别的进程要来读会再叠加共享锁,互相不冲突,此时,如果第三条进程请求写操作,而写得区域又存在别的锁,它将等待这些锁全部解开,然后获取排斥锁,进行修改,此期间对于该区域其他进程的访问都将等待,直到该进程完成修改并释放此排它锁。



上图为排它锁等待



上图为共享锁等待

文件锁还是协同的(advisory)和强制的(mandatory)。协同锁给提供自身的信息给别的请求进程,这种锁并不是操作系统强制的。大部分的UNIX及UNIX-LIKE系统提供了这种锁,其中的一些同时提供了两种锁。强制锁是操作系统强制的,将会阻止其他进程来访问。

除了块式I/O还有一种流式I/O,这种I/O基于管道模型,数据流以字节的方式顺序获取,比块式要慢,通常输入源是断断续续的情况下使用。

  • I/O多路复用(就绪选择)

大部分系统允许流是非阻塞的。意思是,你不会被I/O操作给卡住等待资源准备完成,而可以间断性来检查准备状态是否已经完成。在此基础上再跨前一步,就是就绪选择(readiness selection)的能力。它类似于非阻塞模式并且通常是建立在这个模式之上的机制,除了它将准备状态的检查操作由程序自身丢给操作系统。操作系统被要求去监视一系列流,并且返回准备状态。这种模式允许进程利用共通的代码和一条线程来进行对流的多路复用,这种技术被广泛采纳于网络服务器上,用来处理高并发请求。
0 0
原创粉丝点击