一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Java I/O底层原理及操作

时间:2015-04-23 编辑:简简单单 来源:一聚教程网

缓冲与缓冲的处理方式,是所有I/O操作的基础。术语“输入、输出”只对数据移入和移出缓存有意义。任何时候都要把它记在心中。通常,进程执行操作系统的I/O请求包括数据从缓冲区排出(写操作)和数据填充缓冲区(读操作)。这就是I/O的整体概念。在操作系统内部执行这些传输操作的机制可以非常复杂,但从概念上讲非常简单。我们将在文中用一小部分来讨论它。


上图显示了一个简化的“逻辑”图,它表示块数据如何从外部源,例如一个磁盘,移动到进程的存储区域(例如RAM)中。首先,进程要求其缓冲通过read()系统调用填满。这个系统调用导致内核向磁盘控 制硬件发出一条命令要从磁盘获取数据。磁盘控制器通过DMA直接将数据写入内核的内存缓冲区,不需要主CPU进一步帮助。当请求read()操作时,一旦磁盘控制器完成了缓存的填 写,内核从内核空间的临时缓存拷贝数据到进程指定的缓存中。

有一点需要注意,在内核试图缓存及预取数据时,内核空间中进程请求的数据可能已经就绪了。如果这样,进程请求的数据会被拷贝出来。如果数据不可用,则进程被挂起。内核将把数据读入内存。

虚拟内存

你可能已经多次听说过虚拟内存了。让我再介绍一下。

所有现代操作系统都使用虚拟内存。虚拟内存意味着人工或者虚拟地址代替物理(硬件RAM)内存地址。虚拟地址有两个重要优势:

多个虚拟地址可以映射到相同的物理地址。

一个虚拟地址空间可以大于实际可用硬件内存。

在上面介绍中,从内核空间拷贝到最终用户缓存看起来增加了额外的工作。为什么不告诉磁盘控制器直接发送数据到用户空间的缓存呢?好吧,这是由虚拟内存实现的。用到了上面的优势1。

通过将内核空间地址映射到相同的物理地址作为一个用户空间的虚拟地址,DMA硬件(只能访问物理内存地址)可以填充缓存。这个缓存同时对内核和用户空间进程可见。


这就消除了内核和用户空间之间的拷贝,但是需要内核和用户缓冲区使用相同的页面对齐方式。缓冲区必须使用的块大小的倍数磁盘控制器(通常是512字节的磁盘扇区)。操作系统将其内存地址空间划分为页面,这是固定大小的字节组。这些内存页总是磁盘块大小的倍数和通常为2倍(简化寻址)。典型的内存页面大小是1024、2048和4096字节。虚拟和物理内存页面大小总是相同的。

内存分页

为了支持虚拟内存的第2个优势(拥有大于物理内 存的可寻址空间)需要进行虚拟内存分页(通常称为页交换)。这种机制凭借虚拟内存空间的页可以持久保存在外部磁盘存储,从而为其他虚拟页放入物理内存提供了空间。本质上讲,物理内存担当了分页区域的缓存。分页区是磁盘上的空间,内存页的内容被强迫交换出物理内存时会保存到这里。

调整内存页面大小为磁盘块大小的倍数,让内核可以直接发送指令到磁盘控制器硬件,将内存页写到磁盘或者在需要时重新加载。事实证明,所有的磁盘I/O操作都是在页面级别上完成的。这是数据在现代分页操作系统上在磁盘与物理内存之间移动的唯一方式。

现代CPU包含一个名为内存管理单元(MMU)的子系统。这 个设备逻辑上位于CPU与物理内存之间。它包含从虚拟地址向物理内存地址转化的映射信息。当CPU引用一个内存位置时,MMU决定哪些页需要驻留(通常通过移位或屏蔽地址的某些位)以及转化虚拟页号到物理页号(由硬件实现,速度奇快)。

面向文件、块I/O

文件I/O总是发生在文件系统的上下文切换中。文件系统跟磁盘是完全不同的事物。磁盘按段存储数据,每段512字节。它是硬件设备,对保存的文件语义一无所知。它们只是提供了一定数量的可以保存数据的插槽。从这方面来说,一个磁盘的段与 内存分页类似。它们都有统一的大小并且是个可寻址的大数组。

另一方面,文件系统是更高层抽象。文件系统是安排和翻译保存磁盘(或其它可随机访问,面向块的设备)数据的一种特殊方法。你写的代码几乎总是与文件系统交互,而不与磁盘直接交互。文件系统定义了文件名、路径、文件、文件属性等抽象。

一个文件系统组织(在硬盘中)了一系列均匀大小的数据块。有些块保存元信息,如空闲块的映射、目录、索引等。其它块包含实际的文件数据。单个文件的元信息描述哪些块包含文件数据、数据结束位置、最后更新时间等。当用户进程发送请求来读取文件数据时,文件系统实现准确定位数据在磁盘上的位置。然后采取行动将这些磁盘扇区放入内存中。

文件系统也有页的概念,它的大小可能与一个基本内存页面大小相同或者是它的倍数。典型的文件系统页面大小范围从2048到8192字节,并且总是一个基本内存页面大小的倍数。

分页文件系统执行I/O可以归结为以下逻辑步骤:

确定请求跨越了哪些文件系统分页(磁盘段的集合)。磁盘上的文件内容及元数据可能分布在多个文件系统页面上,这些页面可能是不连续的。

分配足够多的内核空间内存页面来保存相同的文件系统页面。

建立这些内存分页与磁盘上文件系统分页的映射。

对每一个内存分页产生分页错误。

虚拟内存系统陷入分页错误并且调度pagins(页面调入),通过从磁盘读取内容来验证这些页面。

一旦pageins完成,文件系统分解原始数据来提取请求的文件内容或属性信息。

需要注意的是,这个文件系统数据将像其它内存页一样被缓存起来。在随后的I/O请求中,一些数据或所有文件数据仍然保存在物理内存中,可以直接重用不需要从磁盘重读。

文件锁定

文件加锁是一种机制,一个进程可以阻止其它进程访问一个文件或限制其它进程访问该文件。虽然名为“文件锁定”,意味着锁定整个文件(经常做的)。锁定通常可以在一个更细粒度的水平。随着粒度下降到字节级,文件的区域通常会被锁定。锁与特定文件相关联,起始于文件的指定字节位置并运行到指定的字节范围。这一点很重要,因为它允许多个进程协作访问文件的特定区域而不妨碍别的进程在文件其它位置操作。

文件锁有两种形式:共享和独占。多个共享锁可以同时在相同的文件区域有效。另一方面,独占锁要求没有其它锁对请求的区域有效。

流I/O

并非所有的I/O是面向块的。还有流I/O,它是管道的原型,必须顺序访问I/O数据流的字节。常见的数据流有TTY(控制台)设备、打印端口和网络连接。

数据流通常但不一定比块设备慢,提供间歇性输入。大多数操作系统允许在非阻塞模式下工作。允许一个进程检查数据流的输入是否可用,不必在不可用时发生阻塞。这种管理允许进程在输入到达时进行处理,在输入流空闲时可以执行其他功能。

比非阻塞模式更进一步的是有条件的选择(readiness selection)。它类似于非阻塞模式(并且通常建立在非阻塞模式基础上),但是减轻了操作系统检查流是否就绪准的负担。操作系统可以被告知观察流集合,并向进程返回哪个流准备好的指令。这种能力允许进程通过利用操作系统返回 的准备信息,使用通用代码和单个线程复用多个活动流。这种方式被广泛用于网络服务器,以便处理大量的网络连接。准备选择对于大容量扩展是至关重要的。

到此为止,对这个非常复杂的话题有一大堆技术术语。


Java的I/O操作

 
一、概述

  Java的IO支持通过java.io包下的类和接口来完成,在java.io包下主要有包括输入、输出两种IO流,每种输入输出流又可分为字节流和字符流两大类。从JDK1.4以后,Java在java.nio包下提供了系列的全新API,通过java.nio,程序可以更高效的进行输入、输出操作。
二、Java I/O类和接口 

    File类

  File类直接处理文件和文件系统,它没有指定如何获取信息或将信息保存到文件中,只描述了文件本身的属性。File对象用于获得或者操作与磁盘文件相关联的信息,如存取权限、时间、日期和目录路径等,并且还可以浏览子目录的层次结构。

下面的构造函数可用来创建File对象:
构造方法摘要
File(File parent, String child)
          根据 parent 抽象路径名和 child 路径名字符串创建一个新 File 实例。
File(String pathname)
          通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。
File(String parent, String child)
          根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例。
File(URI uri)
          通过将给定的 file: URI 转换为一个抽象路径名来创建一个新的 File 实例。

  File类定义了许多可以得到File对象标准属性的方法

public class Demo
{
    public static void main(String[] args)
    {
        File f=new File("D://hello.java");
        System.out.println(f.getParent());//返回此抽象路径名父目录的路径名字符串;如果此路径名没有指定父目录,则返回 null
        System.out.println(f.getName());//返回由此抽象路径名表示的文件或目录的名称
        System.out.println(f.exists());//测试此抽象路径名表示的文件或目录是否存在
        System.out.println(f.getAbsoluteFile());// 返回此抽象路径名的绝对路径名形式
        System.out.println(f.getAbsolutePath());//返回此抽象路径名的规范路径名字符串
        System.out.println(f.getPath());//将此抽象路径名转换为一个路径名字符串
        System.out.println(f.hashCode());//计算此抽象路径名的哈希码
        System.out.println(f.length());//返回由此抽象路径名表示的文件的长度
        System.out.println(f.list());// 返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录
        System.out.println(f.mkdir());//创建此抽象路径名指定的目录
    }
}

  2. 流类

  Java中把不同的输入输出源抽象画为“流”,通过流的方式允许?Java程序使用相同的方式来访问不同的输入输出源。

  字节流类:提供了处理针对字节的IO的丰富环境,顶部类是InputStream和OutputStream,它们均是抽象类。

  字符流类:字节流类不能处理Unicode字符,字符流类操作的数据单元为字符,,顶部类是Reader和Writer。

  字节流类和字符流类的功能基本一样,只是操作的数据单元不同,其中InputStream和Reader都是将数据抽象为一根水管,程序可以通过read()方法每次抽取一个“水滴”,也可以通过read(char[] cbuf)方法来读取多个“水滴”,程序通过read()方法返回-1来判断是否到了输入流的结束点。

  eg.读取文件,统计文件字符数:

public class FileDemo
{
    public static void main(String[] args)
    {
        int b=0;
        try
        {
            FileInputStream in=null;
            in =new FileInputStream("D:\\a.txt");
            long num=0;
            while((b=in.read())!=-1)
            {
                System.out.print((char)b);
                num++;
            }
            in.close();    
            System.out.println(num);
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        
    }
}

  FileInputStream类创建一个InputStream,可以用来从文件中读取文件。

  eg.将一个文件内容拷贝至另一个文件:

public class FileOutStream
{
    public static void main(String[] args)
    {
        int b=0;
        try
        {
            FileInputStream in =new FileInputStream("D:\\Eclipse\\workSpace\\day_041602\\src\\day_041602\\TestMain.java");
            FileOutputStream out=new FileOutputStream("D:\\hello.java");
            while((b=in.read())!=-1)
            {
                out.write(b);
            }
            in.close();
            out.close();
            System.out.println("执行完成");
        }
        catch (FileNotFoundException e)
        {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        } catch (IOException e)
        {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
}

  BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter称为缓冲流,它们通过缓冲输入输出来提高性能。

  eg.在hello.java文本中输入100个随机数,并在屏幕上显示:

public class BufferWriterDemo
{
    public static void main(String[] args)
    {
        try
        {
            String s;
            BufferedWriter bw=new BufferedWriter(new FileWriter("D:\\hello.java"));
            BufferedReader br=new BufferedReader(new FileReader("D:\\hello.java"));
            for(int i=0;i<100;i++)
            {
                s=String.valueOf(Math.random());  //产生随机数
                bw.write(s);//写入hello.java文件中
                bw.newLine();//写入一个换行符
            }
            bw.flush();//刷新缓冲
            while((s=br.readLine()) != null)//读取一个文本行
                {
                        System.out.println(s);
                }
            bw.close();
            br.close();
        }
        catch (IOException e)
        {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
}

 三、NIO
  1.概述

  NIO使用内存映射的方式处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了。

  相关的包有:java.nio.channels包:主要包含Channel和Selector相关的类,java.nio.charset包:主要包含和字符集相关的类

  NIO系基于两个基本的元素:缓冲和通道。缓冲区容纳数据,通道代表队I/O设备的开放式连接。一般而言使用NIO系统,需要获得到I?O设备的一个通道和容纳数据的一个缓冲区,然后可以对缓冲区进行操作,随意输入和输出数据。除此之外,NIO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,还有支持非阻塞式输入输出的Selector类。
  2.缓冲区

   缓冲(buffer)可以理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到buffer中,从channel中读取的数据也必须先读到buffer中。

   Buffer中有三个重要的参数:

        capacity:表示该Buffer的最大存储容量
        limit:第一个不应该被读出或写入的缓冲区位置索引
        position:用于指明下一个可以被读出或写入的缓冲区位置索引

    除此之外还有一个可选的mark标记,该mark允许程序直接将position定位到mark处。位置如下所示:

  技术分享
    每放入一个数据,position向后移动一位,当Buffer装入数据结束后,调用flip方法,将limit设置为position所在的位置,将position设置为0,这样使得从Buffer中读数据总是从0开始。当Buffer输出数据结束后,Buffer调用 clear方法,它将position置为0,,置limit为capacity,这样为再次向Buffer中装入数据做好准备。Buffer还提供了put和get方法,用于向Buffer中放入数据和读取数据,既支持对单个数据的访问也支持对批量数据的访问。

  eg.

public class Test
{
    public static void main(String[] args)
    {
        CharBuffer m=CharBuffer.allocate(8);
        m.put(‘a‘);
        m.put(‘b‘);
        m.put(‘c‘);
        System.out.println("position:"+m.position());
        System.out.println("limit:"+m.limit());
        m.flip();
        System.out.println("第一个元素"+m.get());
        System.out.println("第二个元素"+m.get());
        System.out.println("position:"+m.position());
    }
}

   执行结果:

  技术分享


 3.通道

   Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map方法,通过该map方法可以直接将一块数据映射到内存中。

     Channel是一个接口,系统为该接口提供了FileChannel等实现类,所有的Channel都是通过传统节点InputStream、OutputSteam的getChannel方法来返回对应的Channel。

   Channel中最常见的三个方法是:map、read和write。其中map将Channel对应的部分或全部数据映射的ByteBuffer,read或write方法有一系列重载的形式用于读取数据。

  eg.将WelcomeServlet.java的内容复制到a.txt中去,并在控制台打印处内容。

public class FileChannelTest
{
    public static void main(String[] args)
    {
        FileChannel inChannel=null;
        FileChannel outChannel=null;
        FileChannel randomChannel=null;
        File f=new File("D://WelcomeServlet.java");
        try
        {
            FileInputStream fs=new FileInputStream(f);
            inChannel=fs.getChannel();
            
            MappedByteBuffer buffer=inChannel.map(FileChannel.MapMode.READ_ONLY,0, f.length());//将inChannel里的全部数据映射成ByteBuffer
            Charset charset=Charset.forName("GBK");
            outChannel=new FileOutputStream("D://a.txt").getChannel();
            
            outChannel.write(buffer);
            buffer.clear();
            
            CharsetDecoder decoder=charset.newDecoder();//创建解码器对象
            CharBuffer charBuffer=decoder.decode(buffer);//使用解码器将ByteBuffer转换为charBuffer
            System.out.println(charBuffer);     //获取对应字符串
        }
        catch (FileNotFoundException e)
        {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
        catch (IOException e)
        {
            // TODO 自动生成的 catch 块
            e.printStackTrace();
        }
    }
}


热门栏目