Java中的零拷贝原理

[TOC]

先提出两个问题:
IO过程中,哪些步骤进行了拷贝?哪些地方零拷贝?
Java支持哪些零拷贝?

哪里听说过零拷贝?真的0次拷贝吗?

零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

  1. 零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
  2. 零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销(可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝)。

LinuxI/O机制及零拷贝介绍

IO中断与DMA

IO中断,需要CPU响应,需要CPU参与,因此效率比较低。


Java中的零拷贝原理_第1张图片
image.png

用户进程需要读取磁盘数据,需要CPU中断,发起IO请求,每次的IO中断,都带来CPU的上下文切换。

因此出现了——DMA。
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU 的大量中断负载。

DMA控制器,接管了数据读写请求,减少CPU的负担。这样一来,CPU能高效工作了。
现代硬盘基本都支持DMA。

Java中的零拷贝原理_第2张图片
image.png

Linux IO流程

实际因此IO读取,涉及两个过程:
1、DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
2、用户进程,将内核缓冲区的数据copy到用户空间。

这两个过程,都是阻塞的。

Java中的零拷贝原理_第3张图片
image.png

传统数据传送

Java中的零拷贝原理_第4张图片
image.png

比如:读取文件,再用socket发送出去
传统方式实现:
先读取、再发送,实际经过1~4四次copy。

buffer = File.read 
Socket.send(buffer)

1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

Java中的零拷贝原理_第5张图片
image.png

传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。

重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。

显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。

传统数据传送所消耗的成本:4次拷贝,4次上下文切换。

4次拷贝,其中两次是DMA copy,两次是CPU copy。如下图所示
拷贝是个IO过程,需要系统调用。

Java中的零拷贝原理_第6张图片
image.png

注意一点的是 内核从磁盘上面读取数据 是 不消耗CPU时间的,是通过磁盘控制器完成;称之为DMA Copy。

网卡发送也用DMA。

零拷贝的出现

目的:减少IO流程中不必要的拷贝
零拷贝需要OS支持,也就是需要kernel暴露api。虚拟机不能操作内核,

Java中的零拷贝原理_第7张图片
image.png

Linux支持的(常见)零拷贝

一、mmap内存映射

data loaded from disk is stored in a kernel buffer by DMA copy. Then the pages of the application buffer are mapped to the kernel buffer, so that the data copy between kernel buffers and application buffers are omitted.

DMA加载磁盘数据到kernel buffer后,应用程序缓冲区(application buffers)和内核缓冲区(kernel buffer)进行映射,数据再应用缓冲区和内核缓存区的拷贝就能省略。

Java中的零拷贝原理_第8张图片
image.png

mmap内存映射将会经历:3次拷贝: 1次cpu copy,2次DMA copy;
以及4次上下文切换

二、sendfile

linux 2.1支持的sendfile

when calling the sendfile() system call, data are fetched from disk and copied into a kernel buffer by DMA copy. Then data are copied directly from the kernel buffer to the socket buffer. Once all data are copied into the socket buffer, the sendfile() system call will return to indicate the completion of data transfer from the kernel buffer to socket buffer. Then, data will be copied to the buffer on the network card and transferred to the network.

当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer;

一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。
socket buffer里的数据就能在网络传输了。

Java中的零拷贝原理_第9张图片
image.png

sendfile会经历:3次拷贝,1次CPU copy 2次DMA copy;
以及2次上下文切换

三、Sendfile With DMA Scatter/Gather Copy

Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.

Scatter/Gather可以看作是sendfile的增强版,批量sendfile。

Java中的零拷贝原理_第10张图片
image.png

Scatter/Gather会经历2次拷贝: 0次cpu copy,2次DMA copy

IO请求批量化

DMA scatter/gather:需要DMA控制器支持的。
DMA工作流程:cpu发送IO请求给DMA,DMA然后读取数据。
IO请求:相当于可以看作包含一个物理地址。
从一系列物理地址(10)读数据:普通的DMA (10请求)
dma scatter/gather:一次给10个物理地址, 一个请求就可以(批量处理)。

Linux零拷贝机制对比

无论是传统IO方式,还是引入零拷贝之后,2次DMA copy 是都少不了的。因为两次DMA都是依赖硬件完成的。

Java中的零拷贝原理_第11张图片
image.png

零拷贝的广义狭义之分

Java中的零拷贝原理_第12张图片
image.png

实际上,零拷贝时有广义和狭义之分的。

广义零拷贝: 能减少拷贝次数,减少不必要的数据拷贝,就算作“零拷贝”。
这是目前,对零拷贝最为广泛的定义,我们需要知道的是,这是广义上的零拷贝,并不是操作系统 意义上的零拷贝。

零拷贝的广义性
最早的零拷贝定义,来源于

Linux 2.4内核新增 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。这是真正操作系统 意义上的零拷贝(也就是狭义零拷贝)。

但是我们知道,由OS内核提供的 操作系统意义上的零拷贝,发展到目前也并没有很多种,也就是这样的零拷贝并不是很多;

随着发展,零拷贝的概念得到了延伸,就是目前的减少不必要的数据拷贝都算作零拷贝的范畴;

Java零拷贝机制解析

Linux提供的领拷贝技术 Java并不是全支持,支持2种(内存映射mmap、sendfile);

Java中的零拷贝原理_第13张图片
image.png

NIO提供的内存映射 MappedByteBuffer

  • 首先要说明的是,JavaNlO中 的Channel (通道)就相当于操作系统中的内核缓冲区,有可能是读缓冲区,也有可能是网络缓冲区,而Buffer就相当于操作系统中的用户缓冲区。
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r") 
                                 .getChannel() 
                                .map(FileChannel.MapMode.READ_ONLY, 0, len);

底层就是调用Linux mmap()实现的。

NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,底层就是调用Linux mmap()实现的。

将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝。

使用MappedByteBuffer,小文件,效率不高;一个进程访问,效率也不高。

MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式。
FileChannel.map()是抽象方法,具体实现是在 FileChannelImpl.c 可自行查看JDK源码,其map0()方法就是调用了Linux内核的mmap的API。
使用 MappedByteBuffer类要注意的是:mmap的文件映射,在full gc时才会进行释放。当close时,需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner方法。

NIO提供的sendfile

  • FileChannel.transferTo()方法直接将当前通道内容传输到另一个通道,没有涉及到Buffer的任何操作,NIO中 的Buffer是JVM堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存
  • transferTo()的实现方式就是通过系统调用sendfile() (当然这是Linux中的系统调用)
//使用sendfile:读取磁盘文件,并网络发送
FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open(sa);
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);

ZeroCopyFile实现文件复制

class ZeroCopyFile {

    public void copyFile(File src, File dest) {
        try (FileChannel srcChannel = new FileInputStream(src).getChannel();
             FileChannel destChannel = new FileInputStream(dest).getChannel()) {

            srcChannel.transferTo(0, srcChannel.size(), destChannel);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Java NIO提供的FileChannel.transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。

Kafka中的零拷贝

Kafka两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据。

  • Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
  • Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。

Netty中的零拷贝
Netty中的Zero-copy与上面我们所提到到OS层面上的Zero-copy不太一样, Netty的Zero-copy完全是在用户态(Java层面)的,它的Zero-copy的更多的是偏向于优化数据操作这样的概念

Netty的Zero-copy体现在如下几个个方面:

  • Netty提供了CompositeByteBuf类,它可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。
  • 通过wrap操作,我们可以将byte[]数组、ByteBuf、 ByteBuffer 等包装成一个 Netty ByteBuf对象,进而避免了拷贝操作。
  • ByteBuf支持slice 操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
  • 通过FileRegion包装的FileChannel.tranferTo实现文件传输,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

前三个都是 广义零拷贝,都是减少不必要数据copy;偏向于应用层数据优化的操作。

你可能感兴趣的:(Java中的零拷贝原理)