零拷贝的优化

267 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

关于拷贝

传统IO

在DMA技术出现之前,数据的拷贝是由CPU直接处理数据的传达,数据拷贝时会直占CPU资源。

image.png

DMA

DMA的全称叫直接内存存取(Direct Memory Access),由CPU向DMA控制器下达指令,让DMA控制器来处理数据的传送,数据传送完毕再把信息反馈给CPU,从而减轻了CPU资源的占有率。DMA需要硬件的支持,目前大多数的硬件设备,包括磁盘控制器、网卡、显卡以及声卡等都支持DMA技术。

image.png

image.png

零拷贝的优化

传统IO

image.png

可以看到,传统IO中出现了4次上下文切换(用户进程读磁盘调用内核、内核读完返回用户进程、用户拿到磁盘中的数据发起传送调用内核、内核传送完了返回用户进程)、2次DMA拷贝、4次CPU拷贝(磁盘数据读取后拷贝到JVM堆外内存、JVM堆外内存拷贝至JVM堆内内存、用户进程发起网络传送JVM堆内内存拷贝至堆外内存、堆外内存拷贝至Socket缓冲区)。

减少用户空间和内核空间的拷贝

mmap+write零拷贝

image.png

使用了mmap将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer) 进行映射,这样不管读还是写都相当于直接作用于内核空间中,如果需要传输就和上图中一样,cpu直接从内核缓冲区拷贝到Socket缓冲区。

简单示例子:

package com.study.nio;

import lombok.SneakyThrows;

import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class MmapReadFile {
    @SneakyThrows
    public static void main(String[] args) {
        FileChannel fileChannel = FileChannel.open(Paths.get("/Users/**/**/test.txt"), StandardOpenOption.READ);
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
        String fileData = StandardCharsets.UTF_8.decode(mappedByteBuffer).toString();
        System.out.println(fileData);
    }
}

减少内核空间的内存复制

Sendfile+DMA Gather Copy零拷贝 image.png

image.png

可以看到用户进程直接发起读磁盘传送数据,只发生2次上下文切换,0次CPU拷贝和2次DMA拷贝。最后的DMA拷贝实际上发生于内核缓冲区内。如果需要进行对数据的写就需要使用mmap进行映射,用户进程直接对内核缓冲区进行写。

splice系统调用可以再内核的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline),从而避免了两者之间的CPU拷贝操作。

image.png

简单示例子:

package com.study.nio;

import lombok.SneakyThrows;

import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class TransferToFile {
    @SneakyThrows
    public static void main(String[] args) {
        FileChannel fileChannel = FileChannel.open(Paths.get("/Users/**/**/test.txt"), StandardOpenOption.READ);
        fileChannel.transferTo(0, fileChannel.size(), FileChannel.open(Paths.get("/Users/**/**/test-copy.txt"), StandardOpenOption.WRITE));
    }
}

减少JVM堆内存的内存复制

image.png

改成直接用堆外内存,避免从JVM堆复制到JNI堆。 image.png

简单示例子:

package com.study.nio;

import lombok.SneakyThrows;

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class DirectByteBufferReadFile {
    @SneakyThrows
    public static void main(String[] args) {
        FileChannel fileChannel = FileChannel.open(Paths.get("/Users/naigaipaopao/Downloads/test.txt"), StandardOpenOption.READ);
        ByteBuffer directBuffer = ByteBuffer.allocateDirect((int) fileChannel.size());
        fileChannel.read(directBuffer);
        String fileData = StandardCharsets.UTF_8.decode(directBuffer).toString();
        System.out.println(fileData);
    }
}