在开始以前,先介绍一下Linux的IO结构。html
文件系统是内核的功能,是一种工做在内核空间的软件,访问一个文件必需要须要文件系统的存在才能够。Linux 能够支持多达数十种不一样的文件系统,它们的实现各不相同,所以 Linux 内核向用户空间提供了虚拟文件系统这个统一的接口用来对文件系统进行操做。java
虚拟文件系统是位于用户空间进程和内核空间中多种不一样的底层文件系统的实现之间的一个抽象的接口层,它提供了常见的文件系统对象模型(如 i-node, file object, page cache, directory entry, etc.)和访问这些对象的方法(如 open, close, delete, write, read, create, fstat, etc.),并将它们统一输出,相似于库的做用。从而向用户进程隐藏了各类不一样的文件系统的具体实现,这样上层软件只须要和 VFS 进行交互而没必要关系底层的文件系统,简化了软件的开发,也使得 linux 能够支持多种不一样的文件系统。node
上图归纳了一次磁盘 write 操做的过程,假设文件已经被从磁盘中读入了 page cache 中。linux
Block layer 处理全部和块设备相关的操做。block layer 最关键是数据结构是 bio 结构体。bio 结构体是 file system layer 到 block layer 的接口。 当执行一个写操做时,文件系统层将数据写入 page cache(由 block buffer 组成),将连续的块放到一块儿,组成 bio 结构体,而后将 bio 送至 block layer。git
block layer 处理 bio 请求,并将这些请求连接成一个队列,称做 IO 请求队列,这个链接的操做就称做 IO 调度(也叫 IO elevator 即电梯算法).github
Buffer I/O 又被称做Standard I/O,大多数文件系统的默认 I/O 操做都是Buffer I/O。在 Linux 的Buffer I/O 机制中,操做系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的地址空间。Buffer I/O 有如下这些优势:算法
Java中的IO也是Buffer IO。
常见的FileInputStream/FileOutPutStream/RandomAccessFile/FileChannel
,都是Buffer IO。数据库
在 Linux 的实现中,文件 Cache 分为两个层面,一是 Page Cache,另外一个 Buffer Cache,每个 Page Cache 包含若干 Buffer Cache。内存管理系统和 VFS 只与 Page Cache 交互,内存管理系统负责维护每项 Page Cache 的分配和回收,同时在使用 memory map 方式访问时负责创建映射;VFS 负责 Page Cache 与用户空间的数据交换。而具体文件系统则通常只与 Buffer Cache 交互,它们负责在外围存储设备和 Buffer Cache 之间交换数据。Page Cache、Buffer Cache、文件以及磁盘之间的关系以下图所示缓存
写入数据时,则首先将其写入 Page Cache,并将其做为脏页进行管理。 脏页表示数据存储在 Page Cache 中,但须要最总写入底层存储设备。 这些脏页面的内容会异步写入(或者同步系统调用 sync 或 fsync)到底层存储设备。markdown
文件块不只在写入会通过Page Cache,在读取文件时也会被写入到Page Cache中。 例如,当读取一个100兆字节的文件两次时,第二次访问会更快,由于文件块直接来自内存中的 Page Cache,没必要再次从磁盘读取。
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就能够采用指针的方式读写操做这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操做而没必要再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而能够实现不一样进程间的文件共享。以下图所示:
MMAP的读写实际是上也是会通过Cache层的,那么MMAP方式与普通方式(Buffer IO)操做文件的区别是什么呢?
上面已经介绍了Buffer IO的操做方式,MMAP和Buffer IO的区别在于Buffer IO须要如今用户空间下维护一个Buffer区(例如FileChannel读写时使用的Buffer);以写入文件为例,先向用户空间下的Buffer写入数据,而后再拷贝到内核缓冲(page cache),而MMAP直接将文件(确切的说应该是文件对应的Page Cache)映射到进程的地址空间,进程就能够直接之内存的操做方式来操做文件了,不须要用户缓冲到内核缓冲的拷贝。进程对mmap的操做至关于直接操做了cache,读取mmap时等于直接读取cache,写入mmap时等于直接写cache,而后操做系统异步刷盘,固然也能够手动调用sync强制刷盘。少了一次拷贝,速度上天然有提高,因此MMAP又成为零拷贝(ZERO COPY)。
虽然缺点不少,可是若是须要超高性能时仍是须要考虑使用mmap的。
经过FileChannel建立mmap
FileChannel channel = FileChannel.open(new File("your file path").toPath(), StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); //FileChannel.MapMode.READ_WRITE为映射的模式,READ_WRITE表明可读写;0,10为映射的文件偏移,单位字节 MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 10); //MappedByteBuffer继承于NIO的ByteBuffer,读写数据的接口和ByteBuffer一致,注意:MappedByteBuffer实际上属于堆外内存([Direct Buffer][5]) mappedByteBuffer.putInt(1); mappedByteBuffer.put((byte) 0x01); mappedByteBuffer.putLong(1l); //对于mmap的写入,都是写入在cache中的,操做系统会异步刷盘,固然若是对数据一致性有严格要求,能够手动调用force强制刷盘,可是这样性能就很是差了。 mappedByteBuffer.force();
Java中对于MMAP的释放没有一个优雅的方式,释放起来比较麻烦,下面贴一个释放的工具类:
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; public final class ByteBufferSupport { private static final MethodHandle INVOKE_CLEANER; static { MethodHandle invoker; try { // Java 9 added an invokeCleaner method to Unsafe to work around // module visibility issues for code that used to rely on DirectByteBuffer's cleaner() Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); invoker = MethodHandles.lookup() .findVirtual(unsafeClass, "invokeCleaner", MethodType.methodType(void.class, ByteBuffer.class)) .bindTo(theUnsafe.get(null)); } catch (Exception e) { // fall back to pre-java 9 compatible behavior try { Class<?> directByteBufferClass = Class.forName("java.nio.DirectByteBuffer"); Class<?> cleanerClass = Class.forName("sun.misc.Cleaner"); Method cleanerMethod = directByteBufferClass.getDeclaredMethod("cleaner"); cleanerMethod.setAccessible(true); MethodHandle getCleaner = MethodHandles.lookup().unreflect(cleanerMethod); Method cleanMethod = cleanerClass.getDeclaredMethod("clean"); cleanerMethod.setAccessible(true); MethodHandle clean = MethodHandles.lookup().unreflect(cleanMethod); clean = MethodHandles.dropArguments(clean, 1, directByteBufferClass); invoker = MethodHandles.foldArguments(clean, getCleaner); } catch (Exception e1) { throw new AssertionError(e1); } } INVOKE_CLEANER = invoker; } private ByteBufferSupport() { } public static void unmap(MappedByteBuffer buffer) { try { INVOKE_CLEANER.invoke(buffer); } catch (Throwable ignored) { throw Throwables.propagate(ignored); } } }
经过Direct I/O 方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,彻底不须要Page Cache的支持。操做系统层提供的缓存每每会使应用程序在读写数据的时候得到更好的性能,可是对于某些特殊的应用程序,好比说数据库管理系统这类应用,他们更倾向于选择他们本身的缓存机制,由于数据库管理系统每每比操做系统更了解数据库中存放的数据,数据库管理系统能够提供一种更加有效的缓存机制来提升数据库中数据的存取性能。下图是Direct IO的路径:
JDK并无提供对Direct IO的支持(但C++使用很简单),须要经过JNA的方式来调用,这里推荐两个DIO库
关于Cache层的位置,查了不少资料,但说法不一致。有说Cache层在VFS和FS之间的,有说在FS之下的,但这不是很重要,咱们只须要认为Cache层在VFS/FS之下就能够了。
适用于普通类型的文件读写,性能尚可,操做简单,无注意事项。
小数据量读写性能高,但不灵活。
须要本身控制Cache时,能够适用Direct IO,例如数据库/中间件应用,能够避免文件的读写还通过一层Page Cache,形成额外开销。