26 June 2013

IO,Input Output的缩写。

IO根据操作数据来源的介质不同,可以分为 Disk IO,Network IO等等.

本文描述的对象主要是Disk IO.

根据JAVA IO的时间演进顺序,分为 OIO和NIO。


OIO(Old IO)

Inputstream 及其子类主要负责将磁盘数据读入到字节数组中。

Outputstream 及其子类主要负责将字节数组写出到磁盘中。

ReaderWriter和前两者类似,不过主要负责读写字符

相同的字节在不同的编码方式下,可以显示为不同的字符。

由于Disk IO的读写数据的速度远远低于CPU处理数据的速度,所以通常在读写时,通常封装一层Buffer。

通常我们使用InputStreamReaderOutputStreamReader的带字符集参数的构造方法,完成将字节流到字符流的转换。

慎用FileReader,因为其不能指定文件读写编码方式,容易导致乱码。

之所以会出现乱码,大部分时因为互相转换字节,字符时,没有指定正确的编码(通常是使用了默认得编码,比如String.getBytes默认使用JVM的默认编码,J2EE容器如tomcat使用了server.xml的ISO-8859-1的编码).


OIO存在的一些缺陷

需要知道OIO存在哪些缺陷的话,我们需要知道底层的IO底层大概是什么样子的。下面的几个章节补充一些IO基本原理。

背景知识之缓冲区操作

以用户进程A发起读数据操作为例(忽略了很多细节)

  1. 用户进程使用read()系统调用,要求填满缓冲区
  2. 内核向磁盘控制器发出命令,要求其从磁盘读取数据
  3. 磁盘控制器把数据写入内核的内存缓冲区(通过DMA完成,不需要CPU参加
  4. 一旦磁盘控制器把缓冲区装满,内核即把数据从内核的缓冲区复制到用户进程的缓冲区,至此完成read()系统调用。

需要注意如下几点:

  1. 用户空间进程一般无法直接访问硬件,内核进程则可以。
  2. 之所以需要把内核的缓冲区数据复制到用户进程的缓冲区,是因为:硬件一般无法访问用户进程,其原因之一是硬件一般无法使用虚拟内存;内核充当中间人的角色,负责把一些数据进行分解和再组合等工作。

背景知识之虚拟内存

虚拟内存是指使用虚拟的地址取代物理(RAM)内存地址。这样做的好处主要有:

  1. 同一个物理地址可以被映射成多个不同的虚拟地址(这样可以避免上述的内核缓冲区复制到用户空间缓冲区的问题)
  2. 虚拟内存地址空间可以大于实际的物理地址空间(这样,就可以在系统物理内存不够用时,根据一些策略将不常用的进程占用的内存备份到磁盘上,然后将节省出来的这部分内存供需要的进程使用。这种技术被称为“内存页面调度”,并且在这种技术中,通常会产生一个“缺页中断”。)

背景知识之文件系统

磁盘属于硬件设备,磁盘把数据存在扇区上,通常一个扇区512字节。

文件系统时更高层次的抽象,文件系统定义了文件名、路径、文件、文件属性等等。当用户进程请求读取文件数据时,文件系统需要确定数据具体存在磁盘的什么位置,然后开始把相关磁盘扇区读进内存

采用分页技术的操作系统执行IO的全过程可以分为如下几个步骤:

  1. 确定请求的数据分布在文件系的哪些页(磁盘扇区组)。磁盘上的文件内容和元数据可能跨越多个文件系统页,而且这些页也可能不连续。
  2. 在内核空间分配足够数量的内存页,以容纳得到确定的文件系统页。
  3. 在内存页与磁盘上的文件系统页之间建立映射
  4. 为每一个内存页产生页错误
  5. 虚拟内存系统捕获也错误,安排页面调入,从磁盘上读取页内容
  6. 一旦页面调入操作完成,文件系统即对原始数据进行解析,取得所需文件内容信息。

操作系统一般会预读额外的文件系统页以提升性能。

文件系统页可能在相当长的时间内继续有效,这样该文件在后续被进程打开时,可能根本无需访问磁盘。

文件修改时,会及时将脏页写到磁盘上。


背景知识之流IO

I/O 字节流必须顺序存取,常见的例子有 TTY(控制台)设备、打印机端口和网络连接。

流的传输一般比块设备慢,经常用于间歇性输入。多数操作系统允许把流置于非块模式,这样,进程可以查看流上是否有输入,即便当时没有也不影响它干别的。这样一种能力使得进程可以在有输入的时候进行处理,没有输入流的时候执行其他功能。 比非块模式再进一步,就是就绪性选择。就绪性选择与非块模式类似(常常就是建立在非块模式之上),但是把查看流是否就绪的任务交给了操作系统。操作系统受命查看一系列流,并提醒进程哪些流已经就绪。这样,仅仅凭借操作系统返回的就绪信息,进程就可以使用相同代码和单一线程,实现多活动流的多路传输。这一技术广泛用于网络服务器领域,用来处理数量庞大的网络连接。就绪性选择在大容量缩放方面是必不可少的。


OIO的缺陷

在大多数情况下,Java 应用程序并非真的受着 I/O 的束缚。操作系统并非不能快速传送数据,让 Java 有事可做;相反,是 JVM 自身在 I/O 方面效率欠佳。操作系统与 Java 基于流的 I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。而 JVM 的 I/O 类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io 的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io 类则喜欢一铲子一铲子地加工数据。有了 NIO,就可以轻松地把一卡车数据备份到您能直接使用的地方(ByteBuffer 对象)。

没有提供当今大多数操作系统普遍具备的常用 I/O 功能,如文件锁定、就绪性选择和内存映射。


NIO(New IO)


Buffer

每个非布尔原始数据类型都有一个缓冲区类,即为ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。


Buffer的四个属性

所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。它们是:

  1. 容量(Capacity) 缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
  2. 上界(Limit) 缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数,指明了缓冲区有效内容的末端
  3. 位置(Position) 下一个要被读或写的元素的索引。位置会自动由相应的get( )和put( )函数更新
  4. 标记(Mark) 一个备忘位置。调用mark( )来设定mark = postion。调用reset( )设定position = mark。标记在设定前是未定义的(其值为-1)。mark 提供标记,类似书签功能,方便重读.
  5. 这4个属性大小关系满足: mark <=position <=limit<=capacity

Buffer的重要方法

  1. flip():该方法通常是读取读取后,完成将数据写到buffer中;然后在需要将buffer的数据读出来,供应用程序使用;如果通道现在在缓冲区上执行get(),那么它将从我们刚刚插入的有用数据之外取出未定义数据,所以这个时候就需要先调用flip方法,将limit=position,position=0,mark=-1,这样就可以使程序顺利的从第position个位置开始读数据。其效果基本等同于buffer.limit(buffer.position()).position(0);
  2. rewind():该方法与flip()相似,但不影响limit属性。它只是将position设回0,以便重读缓冲区中的数据。
  3. clear():方法并没有清除掉原来的buffer中的数据,仅仅是改变属性值:position=0,limit=capacity,mark=-1

Channel

Channel的常见实现类如下:

  • FileChannel
  • ServerSocketChannel
  • SocketChannel
  • DatagramChannel

由于本文主要讲Disk IO,所以重点介绍FileChannel,其他3类会略带提过。


如何创建FileChannel对象

不能直接创建 FileChannel 对象,后3种可以通过调用相应里地类方法open进行创建Channel对象。FileChannel对象却只能通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel( )方法来获取。

Channels 提供了将流和通道之间的互相转换API。


为什么FileChannel总是阻塞的

最主要原因是文件IO延迟较少,详细见下分析:

非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。将非阻塞I/O和选择器组合起来可以使您的程序利用多路复用I/O(multiplexed I/O)。

文件通道总是阻塞式的,因此不能被置于非阻塞模式。现代操作系统都有复杂的缓存和预取机制,使得本地磁盘I/O操作延迟很少。网络文件系统一般而言延迟会多些,不过却也因该优化而受益。

面向流的I/O的非阻塞范例对于面向文件的操作并无多大意义,这是由文件I/O本质上的不同性质造成的。对于文件I/O,最强大之处在于异步I/O(asynchronous I/O),它允许一个进程可以从操作系统请求一个或多个I/O操作而不必等待这些操作的完成。发起请求的进程之后会收到它请求的I/O操作已完成的通知。异步I/O是一种高级性能,当前的很多操作系统都还不具备。以后的NIO增强也会把异步I/O纳入考虑范围。


FileChannel的重要方法

  1. force(boolean metaData):该方法将数据强制刷新到本次存储设备上,这对确保在系统崩溃时不会丢失重要信息特别有用。metaData 参数可用于限制此方法必需执行的 I/O 操作数量。为此参数传入 false 指示只需将对文件内容的更新写入存储设备;传入 true 则指示必须写入对文件内容和元数据的更新,这通常需要一个以上的 I/O 操作。此参数是否实际有效取决于底层操作系统,因此是未指定的。
  2. map(MapMode, long, long):将此通道的文件区域直接映射到内存中,并返回MappedByteBuffer(通常是DirectByteBuffer)。仅建议在读写相对较大的文件时使用此方法,读写小文件时建议使用read/write方法。可以调用DirectByteBuffer的接口方法clean来及时回收内存。
  3. transferTo(long, long, WritableByteChannel):该方法内部使用了zero-copy机制,避免了不必要的上下文切换开销和缓存区数据复制开销,能够极大的提高传输文件能力。详见参考链接。

参考

http://ifeve.com/java-nio-all/

http://blog.csdn.net/historyasamirror/article/details/5778378

http://www.ibm.com/developerworks/cn/java/j-lo-javaio/

http://blog.csdn.net/tabactivity/article/details/9317143

Java NIO,概述部分总结的相当精彩,后面有些章节翻译的比较粗糙.

http://www.ibm.com/developerworks/cn/java/j-zerocopy/,通过零拷贝实现有效数据传输



blog comments powered by Disqus