说起计算机系统中的输入输出,我们在应用开发中用到的一般都是外部数据源与计算机中央处理单元之间的数据输入和输出。
我们编写的大部分应用程序基本都会涉及到数据的输入和输出操作过程。 包括将数据从外部存储读取到处理器管理调度的内部存储空间,以及将内部存储空间的数据写出到外部存储系统。
当然除了外部存储系统外,还有通过网络连接的其它应用程序之间的通信,也存在着输入和输出这样的过程,这样的输入一般都是其它应用通过网络数据流将数据读入内部存储空间,或者将数据从内部缓冲空间写出到网络数据流中。
从硬件的角度来说,输入输出操作一般由CPU来完成,但是随着硬件结构的发展,项各类存储设备和网络设备的出现,有了众多的输入输出控制设备。
它们可以在没有CPU参与的情况下直接通过数据总线跟系统的主内存进行数据的交互,我们称之为直接内存访问(DMA)技术。
在最初的Java编程中对于输入输出功能的支持是由CPU来执行的,该过程是CPU在执行数据输入时将外部数据先读入操作系统管理的内存缓冲区,然后在拷贝到我们应用程序运行进程管理的内存缓冲区,这个过程CPU是全程参与的,被称为I/O导向型处理器。
也就是说处理该操作的线程不结束,CPU做不了其它事情。
当我们的操作系统支持DMA来进行输入输出的读写时,CPU对输入输出的处理就编程了响应就绪选择事件并交由相对应的线程处理,真正的数据流读写则是由DMA控制器来完成。
这样我们的CPU就不用再在数据输入/输出操作期间被独占,而是被解放出来执行其它任务。
关于数据流
=====
在数据输入输出描述中,我们抽象出了一个概念叫做流Stream, 简单数来就是从一个点到另外一个点的数据有序流动,或者说是一个任意长度的有序字节序列。
在Java编程中,我们为了更好的管理数据流动,将流分为输入流和输出流,并抽象了两个接口定义InputStream和OutputStream来分别描述它们。
因为我们计算机底层对于数据处理的基本单位是字节byte,所以我们数据流的基本单元也是比特,我们也称这样的数据流为字节流。
每个数据流都有两个端点,分别为数据源和数据目的地。通常它们可以是文件,网络数据流等。
很多小伙伴在学习Java编程时,很容易被I/O这部分的一些列概念定义搞混了。因为所有的数据流都是以字节流为基础根据各类数据类型的定义进行的编码处理后的结果。
在理解Java的I/O类型时,需要先了解一个设计模式,那就是装饰器模式。
简单来说,装饰器模式是通过一个基础的接口来描述所有接口约定,然后用基础实现类和附加实现类结合共同实现该接口,如此一来我们就可以保留原有接口实现的功能,并且能够通过实现类来提供附加处理功能,并提供统一的对外接口。
Java的输入输出流类型定义就遵循的这一模式,数据流的基础实现ByteInputStream和ByteOutputStream作为基础类,增加了继承继承接口的过滤功能接口和缓冲功能接口,通过它们的实现类我们可以对本来只能处理单个字节的数据流类变为可以通过特定缓冲区来缓存一定数量的字节后进行处理的BufferStream以及缓存后对整体数据添加条件过滤的BufferFilterStream。
进而我们根据各种数据类型的编码规则,对基础的字节流进行编解码处理,定义出了字符,整型,长整型,浮点型等能够处理各基础类型的数据流类型定义。
标准 输入输出
=======
许多操作系统都支持这种标准输入/输出,它是在开始执行时,计算机程序和它运行环境之间预先连接的输入输出流。
这种预连接的流通常有标准输入,标准输出和标准错误流。
最常见的实现了编解码功能的就是我们常说的标准流,Java编程中我们从java.system中能够看到in,out,err等标准I/O流的定义。
标准输入默认从键盘读取它的输入。
标准输出和标准错误默认将它们的输出到屏幕上。
数据流的分类
======
说到数据流就不得不说我们常见的数据流类型,通常我们处理的数据主要分为两种类型,一种是基于文件存储块的块类型数据流,另一种是基于网络字节流的流类型数据流。
而我们编写的几乎所有的应用程序都会或多或少的涉及到这两种类似数据流的处理,当然也有极少数特殊的应用程序不需要我们操作数据流。
我们知道计算机操作系统中是以文件系统作为数据存储和操作的基础组件的,文件系统设计是基于数据存储的物理磁盘结构来设计的,因为磁盘是以固定的分区块来管理数据存取的。
所以基于文件的数据流都是面向块操作的数据流,而像基于网络访问的数据流动则是面向字节流的数据流。
很明显面向块的数据流处理要比面向流的数据处理要快速。
关于NIO模型
=======
前面讲到Java开发最初对于I/O的支持模式是单线路的,也就是说需要CPU全程参与数据读取过程。每个数据输入或者输出的处理线程都需要CPU来阻塞执行。即一旦开始读写,CPU将等待其结束,然后才能进行其它任务执行。
随着技术的发展,NIO模型的出现,该模型巧妙的设计了一个内存缓冲区和通道概念,将原来针对具体数据流的操作,封装成了对于缓冲区的操作。并将所有操作都抽象到一个通道模型里。
其中引入的缓冲区概念Buffer是新一代输入输出操作模型的基础,它本质上是一块内存空间的抽象,并将其具体的操作指令化封装给连接到它的通道Channel上。
由通道来封装跟这内存区域关联的数据源或者目的地。通道的read()和write()方法来触发CPU对读写事件的响应处理。
具体就是在read()指令发出后,操作系统会将数据从数据源读入到指定的缓冲区里,而当write()指令发出后,则会将连接到该通道的缓冲区数据排空出缓冲区到指定的数据目的地。
如此我们就不难理解,我们可以将基于任意数据源建立的流封装成对应的数据通道,然后为通道指定连接的缓冲区,接下来就是发送相关指令来进行目标数据的操作了。
我们使用Java编写的所有应用程序都是受到JVM进程管理的,所以跟操作系统的通信也是有JVM负责完成的。
JVM在执行I/O时,通过请求操作系统执行一个write操作排空缓冲区内容到存储器,使用一个读操作来从存储设备读取数据填充缓冲区空间。
假设我们的读操作包含一个硬盘驱动步骤,操作系统会发布一个命令给硬盘控制器来从硬盘上读取一个字节块到操作系统的缓冲区。
一旦这一操作完成,操作系统会拷贝其缓冲区的内容到由我们发起的read()操作的进程指定的缓冲区里。
如果我们的应用程序进程发布一个read()方法调用操作系统,那么操作系统会请求硬盘控制器来从硬盘上读取数据字节块。
DMA技术问题
=======
这里硬盘控制器就是通过前面提到的直接内存访问技术(DMA)来将数据从硬盘读取到操作系统的缓冲区里。
因为DMA能够允许特定的硬件子系统独立于CPU而直接访问主系统内存。
其实,这种从操作系统缓冲区拷贝字节到应用程序进程缓冲区并不是一个高效的方式。
如果让DMA控制器直接将数据拷贝到进程缓冲区会更高效,但是我们在用它编程过程中会遇到两个问题:
DMA控制器通常不能直接跟运行JVM进程的用户空间进行交互,它只能跟操作系统的内核空间交互。
一般面向块存储的设备,其操作的数据块是固定大小的,而我们应用程序的JVM进程可能需要的数据类型大小不一定就是存储块的倍数,这就会出现数据流类型的错配。
所以,必须在直接访问控制器的数据与具体应用程序的数据之间有一个适配过程,而这个过程就是由我们的操作系统来执行的。
通常我们的操作系统会在JVM进程和DMA控制器之间数据转换时对数据进行分解和重组,使得它们之间能够顺畅的通信。