IO 零拷贝

本贴最后更新于 773 天前,其中的信息可能已经事过景迁

1.传统 IO 拷贝

image.png

如图所示,发生 4 次上下文切换以及 4 次数据拷贝(2 次 CPU 拷贝以及 2 次 DMA 拷贝)

2.零拷贝的几种方式

  • mmap+write
  • sendfile
  • 带有 DMA 收集拷贝功能的 sendfile

2.1mmap+write

虚拟内存

现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有 2 个好处:

  • 虚拟内存空间可以远远大于物理内存空间
  • 多个虚拟内存可以指向同一个物理地址

正是多个虚拟内存可以指向同一个物理地址 ,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少 IO 的数据拷贝次数啦,示意图如下

image.png

mmap

97ccce05c5a23a72ba9e48a83f34efcc.png

  • 用户进程通过 mmap方法 向操作系统内核发起 IO 调用,上下文从用户态切换为内核态
  • CPU 利用 DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
  • 上下文从内核态切换回用户态 ,mmap 方法返回。
  • 用户进程通过 write 方法向操作系统内核发起 IO 调用,上下文从用户态切换为内核态
  • CPU 将内核缓冲区的数据拷贝到的 socket 缓冲区。
  • CPU 利用 DMA 控制器,把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态 ,write 调用返回。

可以发现,mmap+write 实现的零拷贝,I/O 发生了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝

mmap 是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次 CPU 拷贝‘’并且用户进程内存是虚拟的 ,只是映射 到内核的读缓冲区,可以节省一半的内存空间。

2.2 sendfile

10868edf8a0d3ac0f1aae0f8452a1489.png

  1. 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
  2. DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU 将读缓冲区中数据拷贝到 socket 缓冲区
  4. DMA 控制器,异步把数据从 socket 缓冲区拷贝到网卡,
  5. 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回

sendfile 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝 。那能不能把 CPU 拷贝的次数减少到 0 次呢?有的,即 带有DMA收集拷贝功能的sendfile

2.3 sendfile+DMA scatter/gather

5bd5bad036f79070bcda5a6ee6b17a25.png

  1. 用户进程发起 sendfile 系统调用,上下文(切换 1)从用户态转向内核态
  2. DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
  3. CPU 把内核缓冲区中的文件描述符信息 (包括内核缓冲区的内存地址和偏移量)发送到 socket 缓冲区
  4. DMA 控制器根据文件描述符信息,直接把数据从内核缓冲区拷贝到网卡
  5. 上下文(切换 2)从内核态切换回用户态 ,sendfile 调用返回。

可以发现,sendfile+DMA scatter/gather 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝 。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

3. Java 中的零拷贝

3.1 mmap

Java NIO 有一个 MappedByteBuffer 的类,可以用来实现内存映射

try {
	    //读取文件
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
	    //mmap映射读取文件
            MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
	    //获取写入通道
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //数据传输,从mappedBytebuffer中写入到目标通道
            writeChannel.write(data);
            readChannel.close();
            writeChannel.close();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }

3.2 sendfile

FileChannel 的 transferTo()/transferFrom(),底层就是 sendfile() 系统调用函数。Kafka 这个开源项目就用到它

try {
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
            long len = readChannel.size();
            long position = readChannel.position();
        
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //数据传输
            readChannel.transferTo(position, len, writeChannel);
            readChannel.close();
            writeChannel.close();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

4.直接内存

package com.sq.oom;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Date;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@Slf4j
public class OomApplication {

    public static void main(String[] args) {
        SpringApplication.run(OomApplication.class, args);
    }

    @RequestMapping("/test")
    public Object testOom() throws FileNotFoundException {

        //启动参数 -XX:MaxDirectMemorySize=2m -verbose:gc -XX:+PrintGCDetails  限制了最大直接内存为2MB
        //第一次调用没有任何问题,第二次调用直接OOM
        service();
        return "success";
    }

    public void service() throws FileNotFoundException {
        String path = System.getProperty("java.io.tmpdir") + File.separator;
        log.info("file-path={}", path);
        String fileName = "test.DAT";
        String fName = path + fileName;

        File file = new File(fName);
        if (file.exists()) {
            file.delete();
        }
        RandomAccessFile rcf = new RandomAccessFile(fName, "rw");
        FileChannel channel = rcf.getChannel();
        int outLoopSize = 10;
        int innerLoopSize = 2000;
        long offset = 0;

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(2 * 1024 * 1024);

        try {
            for (int i = 0; i < outLoopSize; i++) {
                StringBuilder builder = new StringBuilder();
                for (int j = 0; j < innerLoopSize; j++) {
 
                    builder.append(i+"39xxxxxxxx");
                }
                byte[] bytes = builder.toString().getBytes();
                System.out.println("bytes大小" + bytes.length);
                byteBuffer.put(bytes, 0, bytes.length);
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()) {
                    channel.write(byteBuffer);
                }
                channel.force(true);
                byteBuffer.clear();
                offset += bytes.length;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                channel.close();
                rcf.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

1 操作
AshShawn 在 2022-06-09 20:50:49 更新了该帖

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...