这之后零拷贝没让你输过

2,653 阅读7分钟

零拷贝速览版

普通文件请求

什么叫做零拷贝?先来看看下面这张普通web请求文件的图片。

  1. 先将磁盘里的文件数据读到缓存中(Read Buffer)
  2. 数据返回给应用
  3. 应用将数据发送到缓存中(Socket Buffer)
  4. 最终通过网卡将数据传输到网络请求中

image.png

零拷贝模式请求

直接将数据通过缓存返回给网卡,省去了多余的数据传输和系统操作。

image.png 很明显相比于普通请求,零拷贝少了两次用户态和内核态切换的操作。缓存数据直接从Read Buffer发送,这样做的好处减少了缓存数据的复制。

Java零拷贝代码实现

File srcFile = new File("D:\\零拷贝技术.zip"); 
File descFile = new File("E:\\零拷贝技术.zip"); 
try (FileChannel srcFileChannel = new RandomAccessFile(srcFile, "r").getChannel(); 
FileChannel descFileChannel = new RandomAccessFile(descFile, "rw").getChannel()) { 

srcFileChannel.transferTo(0, srcFile.length(), descFileChannel); 
}

加强版深入解析

深入解析操作系统的零拷贝

首先我们先理解什么是操作系统层面的零拷贝(zero-copy)

为了能更全面的了解零拷贝,我们先看一下操作系统是如何操作文件的。

操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理

我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。

这些系统调用按功能大致可分为如下几类:

  • 设备管理。完成设备的请求或释放,以及设备启动等功能。
  • 文件管理。完成文件的读、写、创建及删除等功能。
  • 进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
  • 进程通信。完成进程之间的消息传递或信号传递等功能。
  • 内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。

这儿我们知道了Java应用操作文件都需要通过系统调用来完成,由Java应用调用操作系统这个过程就是我们所谓的用户态切换内核态的过程,那么什么是用户态和系统态。

  1. 用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。
  2. 系统态(kernel mode):可以简单的理解系统态运行的进程或程序可以访问计算机的任何资源,不受限制。

带着这些基础概念我们在看一次文件读取的流程图

一次基本的磁盘文件数据读取过程如下

  • 当应用程序需要读取磁盘数据时,调用read()从用户态陷入内核态,read()这个系统调用最终由CPU完成;
  • CPU向磁盘发起I/O请求,磁盘收到之后开始准备数据;
  • 磁盘将数据放到磁盘缓冲区之后,向CPU发起I/O中断,报告CPU数据已经Ready了;
  • CPU收到磁盘控制器的I/O中断之后,开始拷贝数据,完成之后read()返回,再从内核态切换到用户态;

image.png 上图中CPU一共参与两次数据拷贝的操作

image.png

我们可以看到,如果应用程序不对数据做修改,从内核缓冲区到用户缓冲区,再从用户缓冲区到内核缓冲区。两次数据拷贝都需要CPU的参与,并且涉及用户态与内核态的多次切换,加重了CPU负担。

们需要降低冗余数据拷贝、解放CPU,这也就是零拷贝Zero-Copy技术。

CPU&DMA方式

直接内存访问(Direct Memory Access),是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU,把之前CPU的杂活让硬件直接自己做了,提高了CPU效率。

目前支持DMA的硬件包括:网卡、声卡、显卡、磁盘控制器等。

image.png

有了DMA的参与之后的流程发生了一些变化:

image.png

最主要的变化是,CPU不再和磁盘直接交互,而是DMA和磁盘交互并且将数据从磁盘缓冲区拷贝到内核缓冲区,之后的过程类似。

解决思路

引入了DMA确实减少了CPU的负担,但是还是每减少用户态切换的损耗。

如何解决呢?目前,零拷贝技术的几个实现手段包括:mmap、sendfile、splice等。这些都可以减少用户态切换的问题

mmap方式

mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享

这样就减少了一次用户态和内核态的CPU拷贝,但是在内核空间内仍然有一次CPU拷贝。

image.png

mmap对大文件传输有一定优势,但是小文件可能出现碎片,并且在多个进程同时操作文件时可能产生引发coredump的signal。

sendfile方式

sendfile系统调用是在 Linux 内核2.1版本中被引入,它建立了两个文件之间的传输通道

image.png

从图中可以看到,应用程序只需要调用sendfile函数即可完成,只有2次状态切换、0次CPU拷贝、2次DMA拷贝。

socket缓冲区中只记录数据描述信息(文件描述符、地址偏移量等信息)。

DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中,从而省去了内核空间中仅剩1次CPU拷贝

splice方式

splice系统调用是Linux 在 2.6 版本引入的,其不需要硬件支持,并且不再限定于socket上,实现两个普通文件之间的数据零拷贝。

splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。

image.png

零拷贝的应用场景

JAVA中的零拷贝-sendfile

  SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("localhost",7001));
    String filename = "text.zip";
//得到一个文件channel
    FileChannel fileChannel = new FileInputStream(filename).getChannel();

//在linux下一个transferTo 方法就可以完成传输
//在windows下 一次调用 transferTo 只能发送8M, 大文件就需要分段传输文件
//transferTo 底层使用到零拷贝
    long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
//关闭
    fileChannel.close();

NIO的零拷贝由transferTo()方法实现。transferTo()方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由native方法transferTo0()来实现,它依赖底层操作系统的支持。在UNIX和Linux系统中,调用这个方法将会引起sendfile()系统调用

JAVA中的零拷贝-mmap

    File file = new File("test.zip");
    RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//获取对应的通道
    FileChannel fileChannel = randomAccessFile.getChannel();
// mmap
    MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size);

NIO的直接内存是由MappedByteBuffer实现的。核心即是map()方法,该方法把文件映射到内存中,获得内存地址addr,然后通过这个addr构造MappedByteBuffer类,以暴露各种文件操作API。

直接内存(mmap技术)将文件直接映射到内核空间的内存,返回一个操作地址(address)它解决了文件数据需要拷贝到JVM才能进行操作的问题,而是直接在内核空间直接进行操作,省去了内核空间拷贝到用户空间这一步操作。

Netty中的零拷贝--FileRegion

image.png

FIleRegion源码中可以看到出现大量zero-copy的说明,主要红框则是零拷贝的实现。这里要说明的是,零拷贝是基于Java直接内存对文件进行操作的一种优化方式。

以下FileRegion底层调用NIO FileChannel的transferTo函数。

@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
  RandomAccessFile raf = null;
  //...
  // 1. 通过 RandomAccessFile 打开一个文件.
  raf = new RandomAccessFile(msg, "r");
  // 通过FileChannel传输
  if (ctx.pipeline().get(SslHandler.class) == null) {
    // 2. 调用 raf.getChannel() 获取一个 FileChannel.
    // 3. 将 FileChannel 封装成一个 DefaultFileRegion
    ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
  } else {
    // SSL enabled - cannot use zero-copy file transfer.
    ctx.write(new ChunkedFile(raf));
  }
  ctx.writeAndFlush("\n");
}

总结:

1.在操作系统层面上的零拷贝是指避免在用户态与内核态之间来回拷贝数据的技术。

2.DMA可以较少一次CPU从磁盘缓存拷贝到内存缓存的操作

3.零拷贝是系统层面的技术,由三种常见实现方式mmap、sendfile、splice

4.零拷贝在Java中的实现主要由NIO中的FileChannel实现,具体api则是transferTo()、map()