前言
上篇更新了linux中IO相关的部分基础知识,主要偏向于理论。本篇会将上章节的理论落地并扩展关于磁盘IO相关的知识点。
如需补充理论知识的同学点击这里。linux之内存管理
一、磁盘IO性能的比较
我们经常说buffered IO比Base IO快,但是我们知道为什么他比基本的IO快吗?
下来我们来看一组代码。简单说明下:在固定时间内,向磁盘中循环写入固定字节数的数据,通过改变输入流的方式,来观察最终生成文件的大小,得出IO性能排名。
package com.dxg.disk;
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class DiskFileIo {
/**
* 输出目录
*/
private static String PATH = "/home/sysio/";
/**
* 输入的数据
*/
private static final byte[] DATA = "123456789\n".getBytes();
/**
* 执行写入的时间
*/
private static final int EXECUTE_TIME = 3;
/**
* 控制写入的状态
*/
private volatile static boolean FLAG = true;
public static void main(String[] args) throws Exception {
DiskFileIo diskFileIo = new DiskFileIo();
switch (args[0]) {
case "1":
diskFileIo.baseWrite("out1.log");
break;
case "2":
diskFileIo.bufferedWrite("out2.log");
break;
case "3":
diskFileIo.randomAccessFileTest1("out3.log");
break;
case "4":
diskFileIo.randomAccessFileMmap("out4.log");
break;
}
}
/**
* 基础文件流写入
*/
public void baseWrite(String fileName) throws Exception {
FileOutputStream outputStream =
new FileOutputStream(new File(PATH + fileName));
startMonit();
while (FLAG) {
outputStream.write(DATA);
}
}
/**
* buffered文件流写入
*
* @param fileName
*/
public void bufferedWrite(String fileName) throws Exception {
BufferedOutputStream outputStream =
new BufferedOutputStream(new FileOutputStream(new File(PATH + fileName)));
startMonit();
while (FLAG) {
outputStream.write(DATA);
}
}
/**
* NIO基础文件写入
*
* @param fileName
* @throws Exception
*/
public void randomAccessFileTest1(String fileName) throws Exception {
RandomAccessFile accessFile = new RandomAccessFile(PATH + fileName, "rw");
startMonit();
while (FLAG) {
accessFile.write(DATA);
}
}
/**
* NIO mmap文件写入
*
* @param fileName
* @throws Exception
*/
public void randomAccessFileMmap(String fileName) throws Exception {
RandomAccessFile accessFile = new RandomAccessFile(PATH + fileName, "rw");
FileChannel channel = accessFile.getChannel();
// 在程序和内核中创建一个共享内存空间
// 调用了内核的mmap
MappedByteBuffer mmap =
channel.map(FileChannel.MapMode.READ_WRITE, 0, Integer.MAX_VALUE);
startMonit();
while (FLAG) {
mmap.put(DATA);
}
}
/**
* 开启监控线程
*/
public void startMonit() {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(EXECUTE_TIME);
FLAG = false;
executor.shutdownNow();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
为使得观察的结果更加精确,我们将源代码copy到linux中。
分别执行命令:
javac DiskFileIo.java && strace -ff -o /home/sysio/trace/trace1 java DiskFileIo 1
strace -ff -o /home/sysio/trace/trace2 java DiskFileIo 2
将源码编译执行并通过strace追踪当前进程的系统调用。执行完成后观察生成的log文件大小如下图:
- out1.log 是通过基础的IO流生成的文件;
- out2.log 是通过buffered IO流生成的文件;
很明显的可以看到在相同执行条件下buffered IO写入速度高于Base IO。此处只能看到buffered IO 比不同IO快,但是我们还是不清楚什么?
我们再执行以上代码时,加入了追踪系统调用的命令,那么我们再在看看系统调用的情况:
- Base IO
- Buffered IO
通过系统调用的对比,可以看到对于Base IO每当我们写入数据时,都会触发一次系统调用,而Buffered IO则是在jvm 的堆中有同一个8K的内存空间,每满8k才会触发一次系统调用写入。程序每触发一次系统调用都触发用户态内核态的切换,这是非常消耗时间的。到此我们开始提出的疑问也得到了答案,Buffered IO 通过减少系统调用来增加IO RW的效率。
不过随着业务量的发展,对于一些大文件Buffered IO 的系统调用也是不容忽视的。对此人们就想到,既然系统调用如此的消耗时间,那么在读写数据时,能不能不彻底去掉系统调用?
技术服务于业务,现在还真有此技术,并且已经很熟练。接着上面的验证,我们继续在服务器上分别执行:
strace -ff -o /home/sysio/trace/trace3 java DiskFileIo 3
strace -ff -o /home/sysio/trace/trace4 java DiskFileIo 4
观察输入的文件大小结果:
哇,这个4太牛了,与1和2都是相同的执行条件和相同的环境,生成的文件竟然比Buffered IO还要大七八倍。忍不住的赶紧去查看的3和4的系统调用文件,如下图:
这个4与1和2都是相同的执行条件和相同的环境,生成的文件竟然比Buffered IO还要大七八倍。简直太牛了,忍不住的赶紧去查看了3和4的追踪文件,发现3和1的追踪文件一致,此处就不展示了。下面看看4的追踪文件,如下图:
从系统调用文件中无法看到像1和2中的write系统调用,确实移除了大量系统调用,增加了IO的读写效率。那么数据文件是如何进入到内核呢?
其实在追踪文件中我们可以看到mmap调用,此调用意为:在进程内和内核中创建一个共享空间,程序和内核共享此块内存区域的数据,且这个内存区域是pagecache到文件的映射。
二、堆内、堆外、共享内存
通过上面的演示,我想大家对于磁盘IO已经渐渐有点感觉了。接下来针对上面的知识做个简单总结以及扩展一些知识点。
在Java 磁盘NIO体系内,对于IO buffer的分配有三种方式:
- 堆内
- 堆外
- 共享内存
ByteBuffer onHeap = ByteBuffer.allocate(1024);
ByteBuffer offHeap = ByteBuffer.allocateDirect(1024);
// 共享内存上面已经演示
下面通过一张图让你更加清晰的了解这三个名词的概念(图解磁盘NIO):
操作系统为每个进程独立开辟了一个虚拟内存地址,每个进程内都具备基础的元素,这些知识在,在上个章节有详细描述。
图中可以很看到堆内内存是存在于JVM的heap上的,堆外内存存在于进程内的堆上,在使用堆内内存中的数据时,需要先将堆内数据先copy到堆外才可以使用,所以在进行创建缓存区时,可以自定义的情况下,优先使用堆外内存。
共享内存中,java进程内的逻辑地址直接映射到内核中的pagecahe且内核中的pagecache直接映射到磁盘中的文件,所以程序中直接通过put调用时不需要通过系统调用。
以上三种分配方式都是收到内核pagecache dirty 的影响,所以说OS没有绝对的数据可靠性。加入pagecahce减少硬件的IO,优先使用内存,以达到提速。但是也带来了丢数据的风险,即使将数据刷新至磁盘的频率调节至0,单机IO负载将会成为瓶颈,所以现在主流的中间件都加入的副本集,以此来保障数据的可靠性。
其实看看吧,理论知识都是相通,要学会举一反三。磁盘IO相关的知识,就先更新到这里。下一章节开始更新网络IO,其中我会讲到BIO到NIO的多种多路复用器直至推导出Netty的IO模型,欢迎大家的关注。