我们作为一名开发,经常耳熟能详的一句话,就是提高程序的性能。对于一个应用程序存在问题的直观体现:页面打开很慢,需要等待。造成这样的问题的主要原因有以下几种:1.网络问题。带宽和网络质量。2.硬件问题。cpu、硬盘、磁盘。3.程序问题。业务逻辑、程序流程、慢 sql。当你的网络稳定、硬件资源充足、程序不存在问题的情况,你还想继续优化应用程序,给用户良好的体验,这时候你需要考虑的就是 IO 的问题。也就是我们常说的磁盘 IO 与网络 IO。
什么是磁盘 IO?举个简单的例子,当你程序的运行,内存中不存在,这个时候应用程序就会触发系统调用,去磁盘中读取相应的数据加载到内存中。这个过程就是一个经典的磁盘 IO。
整个流程如下:
由上图,我们可以看出数据的获取需要 cpu 的参与,数据流向磁盘>>cpu 缓冲区>>内存。且一旦读取的数据量多时,必然需要 CPU 的多次参与,在一定程度上会占用 cpu 的时间片。当数据从磁盘到内存这个过程,无需 cpu 的参与时,那么效率必然更高效。
针对于 IO 中断方式的磁盘 IO,为了减少 cpu 的参与,所以协处理器 DMA 参与的磁盘 IO 应运而生。
具体流程如下:
用户进程调用 read 等系统调用向操作系统发出 IO 请求,请求读取数据到自己的内存缓冲区中。自己进入阻塞状态。
操作系统收到请求后,进一步将 IO 请求发送 DMA。然后让 CPU 干别的活去。
DMA 进一步将 IO 请求发送给磁盘。
磁盘驱动器收到 DMA 的 IO 请求,把数据从磁盘读取到驱动器的缓冲中。当驱动器的缓冲区被读满后,向 DMA 发起中断信号告知自己缓冲区已满。
DMA 收到磁盘驱动器的信号,将磁盘驱动器的缓存中的数据拷贝到内核缓冲区中。此时不占用 CPU(IO 中断这里是占用 CPU 的)。
这个时候只要内核缓冲区的数据少于用户申请的读的数据,内核就会一直重复步骤 3 跟步骤 4,直到内核缓冲区的数据足够多为止。
当 DMA 读取了足够多的数据,就会发送中断信号给 CPU。
CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回。
有了 DMA 的参与,数据可以直接从磁盘拷贝到内存,无需 cpu 的参与。
在磁盘 IO 发生的过程中有几个地方需要我么去了解一下。
1.什么是 DMA?
DMA(Direct Memory Access),直接存储器 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。DMA 可以减少 cpu 的调用。这个机制属于内核优化机制。
2.什么是 pagecache
page cache,又称 pcache,其中文名称为页高速缓冲存储器,简称页高缓。page cache 的大小为一页,通常为 4K。在 linux 读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。从磁盘中读取数据都是以页的形式读取。当程序缓存中不存在需要的数据时,也就是所谓的缺页,也就是触发缺页中断的系统调用,从磁盘中读取数据。一旦程序修改了内存中的数据,那么这个数据页就会被标记为脏,也就是脏页。内核将会在合适的时间把脏页的数据写到磁盘中。多个应用程序对于内存中 page cache 的数据是可以共享,无需重复读取。
尽管有了协处理器 DMA 的参与,但是数据的读取依然存在多次的数据搬运的过程,减少数据多次的搬运,是否能提高程序运行的性能?答案是肯定。如果能减少数据的多次搬运产生的消耗,那么程序运行的时间会更短,所需等待的时间也会越短。mmap 应运而生。
mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
从 IO 中断到 MMAP 的过程,其本质就是减少系统调用以及数据的搬运,从而提高系统的性能,减少资源上的浪费。
对于数据写磁盘的事情也是受到操作系统内核的约束,我们在程序开发的时候,经常性做的一件事件就是将写的操作放到一块,一起写入磁盘,而不是需要写的时候就直接写入,尤其在操作数据库的时候。其本质也是在减少操作系统内核的调用,提升程序的性能。
接下来两段小程序可以证明,数据集中写入可以提高性能。
1.没有buffer的IO
public class OSFileIO {
static byte[] data = "123456789\n".getBytes();
static String path = "/root/testfileio/out.txt";
public static void testBasicFileIO() throws Exception {
File file = new File(path);
FileOutputStream out = new FileOutputStream(file);
while(true){
out.write(data);
}
}
}
2.使用buffer的IO
public class OSFileIO2 {
static byte[] data = "123456789\n".getBytes();
static String path = "/root/testfileio/out1.txt";
public static void testBufferedFileIO() throws Exception {
File file = new File(path);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file));
while(true){
out.write(data);
}
}
}
JVM 中默认的 buffer 大小为 8K。
这两段程序在往文件中写入数据,在规定时间内,可以比较文件大小。使用 buffer 的 IO 的写操作明显比无 buffer 的 IO 快。
所以写磁盘的速度也是:普通 IO<< buffer IO << mmap。
public void whatByteBuffer(){
// ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
System.out.println("postition: " + buffer.position());
System.out.println("limit: " + buffer.limit());
System.out.println("capacity: " + buffer.capacity());
System.out.println("mark: " + buffer);
buffer.put("123".getBytes());
System.out.println("-------------put:123......");
System.out.println("mark: " + buffer);
buffer.flip(); //读写交替
System.out.println("-------------flip......");
System.out.println("mark: " + buffer);
buffer.get();
System.out.println("-------------get......");
System.out.println("mark: " + buffer);
buffer.compact();
System.out.println("-------------compact......");
System.out.println("mark: " + buffer);
buffer.clear();
System.out.println("-------------clear......");
System.out.println("mark: " + buffer);
}
关于文件的读写有几个特别的方法:
pos:读取位置或者放入数据的位置
limit:buffer 的容量,或者可读取的字节数
capicity:总容量的大小。
以上几个方法就是 buffer 数据写入和读取的过程。
1.当程序写操作运行结束,数据是否已经写入磁盘?
答案是否定的,写操作也是受到操作系统内核的限制,在操作系统内核没有触发刷盘操作时,数据是没有真正持久化到磁盘中,依然有丢失数据的问题存在。
2.为什么有 buffer 的 IO 比普通 IO 的数据快?
主要是因为普通的 IO 频繁触发了系统调用,而在 JVM 中只有 buffer 满了,才会触发系统调用,将数据写入磁盘中。
3.当频繁的往一个文件中写入数据,是否会存在内存中?
当内存空间足够时,数据会存在内存中。当内存空间不够时,会优先淘汰掉最早写入的数据。
4.mmap 是否可以不需要操作系统内核直接写入数据?
不可以。mmap 也需要收到内核系统的约束。
vm.dirty_background_ratio = 0 内核往磁盘同步脏页的阈值(百分比)
vm.dirty_background_bytes = 1048576 内核往磁盘同步脏页的字节阈值
vm.dirty_ratio = 0 当程序不停向page cache写入数据达到这个阈值,就会阻塞,开始写入磁盘。
vm.dirty_bytes = 1048576 当程序不停向page cache写入数据达到这个字节限制阈值,就会阻塞,写入磁盘
vm.dirty_writeback_centisecs = 5000 脏页写入磁盘的时间(单位:0.01秒)
vm.dirty_expire_centisecs = 30000 脏页的生命周期可以存多久(单位:0.01秒)
对于磁盘 IO 的读与写,其整个过程都是在减少数据的搬运,操作系统内核的参与,从而提高系统的性能。但是无论采用何种手段,操作系统是没办法保证数据不丢失。其实这是操作系统在数据的可靠性与系统的性能上做了一个取舍。选择了系统性能,承担了在一定时间内丢失数据的风险。作为程序开发,我们的任务就是在操作系统允许的范围内,了解其原理,减少磁盘 IO 中数据的搬运以及系统的调用的次数,提高系统的性能。