标签:
前言
相比I/O,NIO更复杂、更不好理解,因此在开始NIO之前,需要讲解一些概念,如果对于这些概念有着良好的理解,对于学习NIO绝对是有好处的。
同步与异步
所谓同步就是指一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能完成,这是一种可靠的任务序列。要成功都成功,要失败都失败,两个任务的状态可以保持一致。
而异步不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
在涉及I/O处理时通常都会遇到是同步还是异步的处理方式的选择问题,因为同步与异步的I/O处理方式对调用者的影响很大,在数据库产品中都会遇到这个问题,因为I/O操作通常是一个非常耗时的操作,在一个任务序列中I/O通常都是性能瓶颈。但是同步与异步的处理方式对程序的可靠性影响非常大,同步能够保证程序的可靠性,而异步可以提升程序的性能,必须在可靠性和性能之间保持平衡,却没有完美的解决办法。
阻塞与非阻塞
阻塞与非阻塞是从CPU的消耗上来说的。
阻塞就是CPU停下来等待一个慢的操作完成以后,CPU才接着完成其他的工作,在执行这个慢的操作的过程中CPU需要不断轮询操作是否完成。
非阻塞就是在这个慢的操作执行时,CPU去做其他工作,等这个慢的操作完成了,CPU再接着完成后续的操作。
虽然从表面上看非阻塞的方式可以明显地提高CPU的利用率,但是也带来了另外一种后果,就是系统的线程切换的增加,增加的CPU使用时间能不能补偿系统的切换成本需要好好评估。
CPU已不再是束缚
Java程序员把全部精力用在优化处理效率上,而对I/O关注不足,在某种程度上并非他们的错。在Java早期,JVM在解释字节码时往往很少或没有运行时优化,这就意味着,Java程序往往拖得很长,其运行效率大大低于本地编译代码,因而对操作系统I/O子系统的要求不太高。
如今在运行时优化方面,JVM已经前进了一大步,现在JVM运行字节码的速率已经接近本地编译代码,借助动态运行时优化,其表现甚至还有所超越。这就意味着,多数Java应用程序已经不再受CPU的束缚(把大量时间用在执行代码上),而更多时候是受I/O的束缚(等待数据传输)。
然而,在大多数情况下,Java应用程序并非真的受着I/O的束缚。操作系统并非无法快速传送数据,让Java有事可做,而是JVM自身在 I/O方面效率欠佳。操作系统与Java基于流的I/O模式有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。而JVM的I/O类喜欢操作小块数据----单个字节、几行文本。结果,操作系统送来整块缓冲区的数据,java.io的流类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。
有了NIO,就可以轻松把大量数据传输到需要直接使用的地方。不过,这并不是说使用传统的I/O模型无法移动大量数据----当然可以,RandomAccesssFile类就可以,而且效率也不差,只要坚持使用基于数组的read()、write()方法。这些方法与底层操作系统调用相当接近,尽管必须保留至少一份缓冲区拷贝。
NIO的提出
JDK1.4的NIO软件包引入了一套新的抽象用于I/O处理。与以往不同的是,新的抽象把重点放在了如何缩短抽象与现实之间的距离上面。NIO抽象与现实中存在的实体有着非常真实直接的交互关系。要想最大限度地满足Java应用程序密集I/O需求,理解这些新的抽象,以及与其发生交互作用的I/O服务,正是关键所在。
在NIO的学习中,理解以下概念是非常重要的:
缓冲区操作
缓冲区,以及缓冲区如何工作,是所有I/O的基础。所谓"输入/输出"讲的无非就是把数据移入或移出缓冲区。
执行I/O操作,归结起来,也就是向操作系统发出请求,让它要么把缓冲区里的数据读走,要么用数据把缓冲区填满。进程使用这一机制处理所有数据进出操作。操作系统内部处理这一任务的机制,其复杂程序可能超乎想象,但就概念而言,非常简单易懂。
下图描述了数据从外部磁盘向运行中的进程的内存区域移动的过程:
进程使用read()系统调用,要求其缓冲区被填满。内核随机向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据写入内核内存缓冲区,这一步通过DMA完成,无需主CPU协助。一旦磁盘控制器把缓冲区填满,内核即把数据从内核空间的临时缓冲区拷贝到进程read()调用时指定的缓冲区。
注意用户空间和内核空间的概念。用户空间是常规进程所在的区域,JVM就是常规进程,驻守于用户空间。用户空间是非特权区域:比如在该区域执行的代码就不能直接访问硬件设备。内核空间是操作系统所在的区域。内核代码有特别的权利:它能与设备控制器通讯,控制着用户区域进程的运行状态等。最重要的是,所有I/O都直接或间接通过内核空间。
当进程请求I/O操作的时候,它执行一个系统调用将控制权移交给内核。C/C++程序员熟知的底层函数open()、read()、write()、close()要做的无非就是建立和执行适当的系统调用。党内和以这种方式被盗用,它随即采取任何必要步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,改数据只需要简单拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。
可能有人会觉得,为什么不直接让磁盘控制器把数据传送到用户空间的缓冲区呢?这样做有几个问题:
1、硬件通常无法直接访问用户空间
2、像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对其的数据块。在数据往来于用户空间和存储设备的过程中,内核负责数据的分解、再组合工作
虚拟内存
所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可以分为:
1、一个以上的虚拟地址可指向同一个物理内存地址
2、虚拟内存空间可大于实际可用的硬件内存
前面提到,控制设备器不能通过DMA直接存储到用户空间,但通过利用上面提到的第一项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区了:
这样真的太好了,省去了内核空间与用户空间的往来拷贝,但前提是,内核与用户缓冲区必须使用相同的页对齐,缓冲区的大小还必须是磁盘控制器块大小的倍数。
标签:
原文地址:http://www.cnblogs.com/xrq730/p/4896485.html