【杂谈】Java I/O的底层实现

前言

  Java I/O功能封装的很好,使用起来很方便,就是刚开始学的时候,如果不了解装饰器模式,会被他繁多的类给吓到。用多了也就习惯了,而且现在有很多实用的封装良好的实用类,可直接读写整个文件。开发者不知道底层实现细节,也可以灵活使用,这是封装的一大优点。但是,作为一名软件开发人员,对其所使用的代码不能仅仅停留在熟悉功能特性上,最好对其实现原理也要有一定了解。

注:本文引用了部分外文内容,并根据自己的理解进行了翻译,连接将在文末贴出。

 ————————————–外文引用内容Begin(已翻译)—————————————————————-

缓冲处理、内核空间vs用户空间

  缓冲,以及如何处理缓冲是所有IO的基本内容。术语”I/O”(输入输出)指的不过就是从缓冲区移入或移除数据。通常,进程执行I/O操作的方式是,向操作系统发送请求,请求其填充自己的缓冲区(或者把自己缓冲区的内容写出)。这就是I/O这个概念的全部内容。要实现这些传输操作,操作系统底层的实现非常复杂。但是在概念上,本文所要讲述的内容则非常直白。

  

  注意:User space和Kernel space 都属于内存。内存分为两个区,用户区和系统区(内核区)。

  上图简要展示了,块数据如何从外部源头(比如硬盘)移入到进程的内存空间的过程。首先,这个进程通过系统调用read(),请求填充自己的缓冲区。这将导致内核发送一个命令到磁盘控制器,使其从磁盘中抓取数据。磁盘控制器通过DMA把数据直接写入到内核空间缓冲区,这个过程不需要CPU干预。一旦磁盘控制器完成了填充数据的任务,内核就将数据从内核空间的临时缓冲区转移到进程指定的缓冲区内。

  有一件事需要注意,内核会试图缓冲或者说预加载一些数据,所以有可能进程所请求的数据已经在内核空间里了。如果这样的话,进程请求的数据,只需要从内核缓冲区拷贝一份即可。如果数据不在内核空间内,则在内核获取数据到内存的过程中,此进程将被挂起。

————————————–外文引用内容End(已翻译)——————————————————————-

从上述内容可知:

  • Java的读写操作,底层由C/C++实现。而不是直接与OS接触
  • C/C++读写操作,需要OS服务
  • 内核自带缓冲,会过分加载
  • 如果内存中没有数据的缓冲,读写操作将阻塞当前线程(OS会帮你挂起线程)

DMA

   DMA(Direct Memory Access,直接内存存取)是I/O设备控制方式的一种。我个人认为它们的主要差别在于CPU的参与I/O控制的程度

I/O设备控制方式有:

  • 程序I/O方式——CPU需反复检查
  • 中断I/O方式——每完成一个字节的读写,通知CPU
  • DMA方式——每完成一个块(多字节)的读写,通知CPU
  • I/O通道方式(暂不了解)

在DMA读写I/O设备的时候,CPU不会被影响,它可以继续执行。注意!这里能继续执行,指的是CPU可以继续运行,而此I/O操作的线程已经被挂起,不参与CPU调度。I/O操作完成后,该线程才被唤醒,参与调度(加入就绪队列,等待时间片)

系统调用

  系统调用是应用程序间接调用OS函数的方式。C语言有提供与系统调用相对应的库函数。这里就是read、write。 

BufferedXXStream

   注意,对于Java来说,系统调用的开销是比较大的。首先读写操作要触发的是本地方法read0,readBytes,write0,writeBytes,这里JNI需要一定开销。还有就是每产生一个系统调用,就可能产生上千个机器指令,这种开销是不容小觑的。所以,我们要尝试减少系统调用。那有人就会问了,不行啊,我数据又不能缺斤少两,少读少写肯定出问题,怎么减少调用?这不是很好解决吗,每次多读写一点,调用的次数不就少了嘛。而BufferedXXStream就是这么用的,例如,BufferedInputStream的read无参方法只读取一个字节,而实际上BufferedInputStream默认读取了8kb,这些数据用字节数组保留。

      对了,如果对上图,不是很理解,可以看看这张。

   即运行时,有一个对象BufferedInputStream,其调用一次read()方法,数据保留到buf数组中。

 

 

 

 

参考文献  

How Java I/O Works Internally at Lower Level?(需FQ)