支撑百万并发的“零拷贝”技术,你了解吗?( 七 )


下面从 CPU 拷贝次数、DMA 拷贝次数以及系统调用几个方面总结一下上述几种 I/O 拷贝方式的差别:
 
 
JAVA NIO 零拷贝实现
在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区 。
而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer):

  • 通道(Channel)是全双工的(双向传输),它既可能是读缓冲区(read buffer),也可能是网络缓冲区(socket buffer) 。
  • 缓冲区(Buffer)分为堆内存(HeapBuffer)和堆外内存(DirectBuffer),这是通过 malloc() 分配出来的用户态内存 。
堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在 GC 时可能会被自动回收 。
因此,在使用 HeapBuffer 读写数据时,为了避免缓冲区数据因为 GC 而丢失,NIO 会先把 HeapBuffer 内部的数据拷贝到一个临时的 DirectBuffer 中的本地内存(native memory) 。
这个拷贝涉及到 sun.misc.Unsafe.copyMemory() 的调用,背后的实现原理与 memcpy() 类似 。
最后,将临时生成的 DirectBuffer 内部的数据的内存地址传给 I/O 调用函数,这样就避免了再去访问 Java 对象处理 I/O 读写 。
MappedByteBuffer
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式提供的一种实现,它继承自 ByteBuffer 。
FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件 。
抽象方法 map() 方法在 FileChannel 中的定义如下:
  1. public abstract MappedByteBuffer map(MapMode mode, long position, long size)
  2. throws IOException;
Mode:限定内存映射区域(MappedByteBuffer)对内存映像文件的访问模式,包括只可读(READ_ONLY)、可读可写(READ_WRITE)和写时拷贝(PRIVATE)三种模式 。
Position:文件映射的起始地址,对应内存映射区域(MappedByteBuffer)的首地址 。
Size:文件映射的字节长度,从 Position 往后的字节数,对应内存映射区域(MappedByteBuffer)的大小 。
MappedByteBuffer 相比 ByteBuffer 新增了三个重要的方法:
  • fore():对于处于 READ_WRITE 模式下的缓冲区,把对缓冲区内容的修改强制刷新到本地文件 。
  • load():将缓冲区的内容载入物理内存中,并返回这个缓冲区的引用 。
  • isLoaded():如果缓冲区的内容在物理内存中,则返回 true,否则返回 false 。
下面给出一个利用 MappedByteBuffer 对文件进行读写的使用示例:
  1. private final static String CONTENT = "Zero copy implemented by MappedByteBuffer";
  2. private final static String FILE_NAME = "/mmap.txt";
  3. private final static String CHARSET = "UTF-8";
写文件数据:打开文件通道 fileChannel 并提供读权限、写权限和数据清空权限,通过 fileChannel 映射到一个可写的内存缓冲区 mappedByteBuffer,将目标数据写入 mappedByteBuffer,通过 force() 方法把缓冲区更改的内容强制写入本地文件 。
  1. @Test
  2. public void writeToFileByMappedByteBuffer() {
  3. Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());
  4. byte[] bytes = CONTENT.getBytes(Charset.forName(CHARSET));
  5. try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ,
  6. StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
  7. MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_WRITE, 0, bytes.length);
  8. if (mappedByteBuffer != null) {
  9. mappedByteBuffer.put(bytes);
  10. mappedByteBuffer.force();
  11. }
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }
读文件数据:打开文件通道 fileChannel 并提供只读权限,通过 fileChannel 映射到一个只可读的内存缓冲区 mappedByteBuffer,读取 mappedByteBuffer 中的字节数组即可得到文件数据 。
  1. @Test
  2. public void readFromFileByMappedByteBuffer() {
  3. Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());
  4. int length = CONTENT.getBytes(Charset.forName(CHARSET)).length;
  5. try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) {
  6. MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_ONLY, 0, length);
  7. if (mappedByteBuffer != null) {
  8. byte[] bytes = new byte[length];
  9. mappedByteBuffer.get(bytes);
  10. String content = new String(bytes, StandardCharsets.UTF_8);
  11. assertEquals(content, "Zero copy implemented by MappedByteBuffer");
  12. }
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. }
下面介绍 map() 方法的底层实现原理 。map() 方法是 java.nio.channels.FileChannel 的抽象方法,由子类 sun.nio.ch.FileChannelImpl.java 实现 。


推荐阅读