由于,了解了一些异步 IO 的知识,JAVA 里面 NIO 就是原来的 IO 的一个补充,本文主要记录下在 JAVA 中 IO 的底层实现原理,以及对技术介绍.
IO,其实意味着:数据不停地搬入搬出缓冲区而已(使用了缓冲区).比如,用户程序发起读操作,导致 "syscall read" 系统调用,就会把数据搬入到 一个 buffer 中;用户发起写操作,导致 "syscall write" 系统调用,将会把一个 buffer 中的数据 搬出去 (发送到网络中 or 写入到磁盘文件)
上面的过程看似简单,但是底层操作系统具体如何实现以及实现的细节就非常复杂了.正是因为实现方式不同,有针对普通情况下的文件传输 (暂且称普通 IO 吧),也有针对大文件传输或者批量大数据传输的实现方式,比如 zerocopy 技术.
先来看一张普通的 IO 处理的流程图:
整个 IO 过程的流程如下:
1)程序员写代码创建一个缓冲区(这个缓冲区是用户缓冲区):哈哈.然后在一个 while 循环里面调用 read() 方法读数据 (触发"syscall read"系统调用)
byte[] b = new byte[4096];
while ((read = inputStream.read(b)) >= 0) {
total = total + read;
// other code....
}
2) 当执行到 read() 方法时,其实底层是发生了很多操作的:
①内核给磁盘控制器发命令说:我要从读磁盘上的某某块磁盘块上的数据.--kernel issuing a command to the disk controller hardware to fetch the data from disk.
②在 DMA 的控制下,把磁盘上的数据读入到内核缓冲区.--The disk controller writes the data directly into a kernel memory buffer by DMA
③内核把数据从内核缓冲区复制到用户缓冲区.--kernel copies the data from the temporary buffer in kernel space
这里的用户缓冲区应该就是我们写的代码中 new 的 byte[] 数组.
从上面的步骤中可以分析出什么?
ⓐ对于操作系统而言,JVM 只是一个用户进程,处于用户态空间中.而处理用户态空间的进程是不同直接操作底层的硬件的.而 IO 操作就需要操作底层的硬件,比如磁盘.因此,IO 操作必须得借助内核的帮助才能完成 (中断,trap),即:会有用户态到内核态的切换.
ⓑ我们写代码 new byte[] 数组时,一般是都是" 随意 "创建一个" 任意大小 "的数组.比如,new byte[128],new byte[1024],new byte[4096]....
但是,对于磁盘块的读取而言,每次访问磁盘读数据时,并不是读任意大小的数据的,而是:每次读一个磁盘块或者若干个磁盘块 (这是因为访问磁盘操作代价是很大的,而且我们也相信局部性原理) 因此,就需要有一个 "中间缓冲区"-- 即内核缓冲区.先把数据从磁盘读到内核缓冲区中,然后再把数据从内核缓冲区搬到用户缓冲区.
这也是为什么我们总感觉到第一次 read 操作很慢,而后续的 read 操作却很快的原因吧.因为,对于后续的 read 操作而言,它所需要读的数据很可能已经在内核缓冲区了,此时只需将内核缓冲区中的数据拷贝到用户缓冲区即可,并未涉及到底层的磁盘操作,当然就快了.
The kernel tries to cache and / or prefetch data,
so the data being requested by the process may already be available in kernel space.If so,
the data requested by the process is copied out.If the data isn 't available, the process is suspended while the kernel goes about bringing the data into memory. '
如果数据不可用,process 将会被挂起,并需要等待内核从磁盘上把数据取到内核缓冲区中.
那我们可能会说:DMA 为什么不直接将磁盘上的数据读入到用户缓冲区呢?一方面是 ⓑ中提到的内核缓冲区作为一个中间缓冲区 用户缓冲区的 "任意大小" 和每次读磁盘块的固定大小.另一方面则是,用户缓冲区位于用户态空间,而 DMA 读取数据这种操作涉及到底层的硬件,硬件一般是不能直接访问用户态空间的(OS 的原因吧)
综上,由于 DMA 不能直接访问用户空间 (用户缓冲区),普通 IO 操作需要将数据来回地在 用户缓冲区 和 内核缓冲区移动,这在一定程序上影响了 IO 的速度.那有没有相应的解决方案呢?
那就是直接内存映射 IO,也即 JAVA IO 中提到的内存映射文件,或者说 直接内存.... 总之,它们表达的意思都差不多.示例图如下:
来源: