什么是零拷贝?

什么是零拷贝

“零”:表示次数是0,它表示拷贝数据的次数是0
“拷贝”:指数据从一个存储区域转移到另一个存储区域
合起来,那零拷贝就是不需要将数据从一个存储区域复制到另一个存储区域。

零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。

传统IO的执行流程

比如想实现一个下载功能,服务端的任务就是:将服务器主机磁盘中的文件从已连接的socket中发出去,关键代码如下

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
	write(sockfd, buf, n);

传统的IO流程包括read以及write的过程
read:将数据从磁盘读取到内核缓存区中,再拷贝到用户缓冲区
write:先将数据写入到socket缓冲区中,最后写入网卡设备

readwrite函数是系统调用,所以涉及用户态到内核态的切换。

流程图如下

在这里插入图片描述

1.应用程序调用read函数,向操作系统发起IO调用,上下文从用户态切换至内核态
2.DMA控制器把数据从磁盘中读取到内核缓冲区
3.CPU把内核缓冲区数据拷贝到用户应用缓冲区,上下文从内核态切换至用户态,此时read函数返回
4.用户应用进程通过write函数,发起IO调用,上下文从用户态切换至内核态
5.CPU将缓冲区的数据拷贝到socket缓冲区
6.DMA控制器将数据从socket缓冲区拷贝到网卡设备,上下文从内核态切换至用户态,此时write函数返回

从流程图中可以看出传统的IO流程包括***4次上下文的切换***,4次拷贝数据(两次CPU拷贝以及两次DMA拷贝)

前置知识

内核空间和用户空间

我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等等。因为这些都是比较危险的操作,不可以由应用程序乱来,只能交给底层操作系统来。

因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间。内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。 以32位操作系统为例,它会为每一个进程都分配了4G(2的32次方)的内存空间。

内核空间:主要提供进程调度、内存分配、连接硬件资源等功能
用户空间:提供给各个程序进程的空间,它不具有访问内核空间资源的权限,如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成。进程从用户空间切换到内核空间,完成相关操作后,再从内核空间切换回用户空间。

用户态和内核态

如果进程运行于内核空间,被称为进程的内核态。
如果进程运行于用户空间,被称为进程的用户态。

什么是上下文切换

什么是上下文

CPU 寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。它们都是 CPU 在运行任何任务前,必须的依赖环境,因此叫做CPU上下文。

什么是上下文切换

它是指,先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

一般我们说的上下文切换,就是指内核(操作系统的核心)在CPU上对进程或者线程进行切换。进程从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程,会发生CPU上下文的切换。

在这里插入图片描述

虚拟内存

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

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

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

在这里插入图片描述

DMA技术

DMA,英文全称是Direct Memory Access,即直接内存访问。DMA本质上是一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与

简单的说它就是帮住CPU转发一下IO请求以及拷贝数据,那为什么需要它呢?其实主要是效率问题。它帮忙CPU做事情,这时候,CPU就可以闲下来去做别的事情,提高了CPU的利用效率。大白话解释就是,CPU老哥太忙太累啦,所以他找了个小弟(名叫DMA) ,替他完成一部分的拷贝工作,这样CPU老哥就能着手去做其他事情。

下面看下DMA具体是做了哪些工作在这里插入图片描述

1.用户应用程序调read函数,向操作系统发起IO调用,进入阻塞状态等待数据返回
2.CPU接到指令后,对DMA控制器发起指令调度
3.DMA收到请求后,将请求发送给磁盘
4.磁盘将数据放入磁盘控制缓冲区并通知DMA
5.DMA将数据从磁盘控制器缓冲区拷贝到内核缓冲区
6.DMA向CPU发送数据读完的信号,CPU负责将数据从内核缓冲区拷贝到用户缓冲区
7.用户应用进程由内核态切回用户态,解除阻塞状态

如何实现零拷贝

零拷贝并不是没有拷贝数据,而是减少用户态、内核态的切换次数以及CPU拷贝次数;实现零拷贝主要有三种方式分别是

  1. mmap + write
  2. sendfile
  3. 带有DMA收集拷贝功能的sendfile
  4. splice

mmap

mmap的函数原型如下

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset

 addr:指定映射的虚拟内存地址
length:映射的长度
prot:映射内存的保护模式
flags:指定映射的类型
fd:进行映射的文件句柄
offset:文件偏移量

前面一小节,我们介绍了虚拟内存,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝次数!mmap就是用了虚拟内存这个特点,它将内核中的读缓冲区与用户空间的缓冲区进行映射,所有的IO都在内核中完成。mmap+write实现的零拷贝流程如下:

 在这里插入图片描述

1.用户进程通过调用mmap方法向操作系统内核发起IO调用,上下文从用户态切换至内核态
2.CPU利用DMA控制器,将数据从硬盘拷贝到内核缓冲区
3.上下文从内核态切换回用户态,mmap方法返回
4.用户进程通过调用write方法向操作系统内核再次发起IO调用,上下文从用户态切换至内核态
5.CPU将内核缓冲区的数据拷贝到socket缓冲区
6.CPU利用DMA控制器,将数据从socket缓冲器拷贝到网卡,上下文从内核态切换至用户态,write方法返回

可以发现,mmap+write实现的零拷贝其中发生了4次上线文切换以及3次拷贝(2次DMA拷贝+1次cpu拷贝,上下文切换没变,cou拷贝次数减1)

sendfile

sendfile是Linux2.1版本后内核引入 的一个系统调用函数,原型如下

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

out_fd:为待写入内容的文件描述符
in_fd:为待读出内容的文件描述符
offset:文件偏移量
count:指定在fdout和fdin之间传输的字节数

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。sendfile实现的零拷贝流程如下:

在这里插入图片描述

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

可以发现,sendfile实现的零拷贝仅仅发生了2次上下文切换以及3次拷贝(2次DMA拷贝+1次CPU拷贝,注意这里sendfile只有一个系统调用函数了,没了read和write,所以上下文切换减少为2次)

sendfile +DMA scatter/gather实现的零拷贝
linux2.4版本后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡,这样的话还可以省去CPU拷贝。

在这里插入图片描述

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

可以发现sendfile + DMA scatter/gather实现的零拷贝发生了2次上下文切换以及2次数据拷贝,这就是真正的零拷贝技术,全程没有通过CPU来搬运数据,所有的数据都是通过DMA进行传输的。

splice

splice 调用和sendfile 非常相似,与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

  • sendfile主要用于将一个文件发送到套接字,并且不需要经过用户空间缓冲区;而splice则更通用,在两个文件描述符之间进行数据传输。
  • sendfile只能用于文件到套接字的传输,而splice可以在文件和套接字之间自由传输数据。
  • splice函数可以在内核空间中操作,避免了用户空间与内核空间之间的数据拷贝,因此具有更高的性能。

在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。
和 sendfile 不同的是,splice 不需要硬件支持。
 

java提供的零拷贝方式

mmap

Java NIO有一个MappedByteBuffer的类可以用来实现内存映射。它的底层是调用的linux内核的mmap的API

public class MmapTest {

    public static void main(String[] args) {
        try {
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
            MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //数据传输
            writeChannel.write(data);
            readChannel.close();
            writeChannel.close();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}

sendfile

FileChannel的transferTo()/transferFrom(),底层就是sendfile() 系统调用函数。Kafka 这个开源项目就用到它,平时面试的时候,回答面试官为什么这么快,就可以提到零拷贝sendfile这个点

public class SendFileTest {
    public static void main(String[] args) {
        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());
        }
    }
}

哪些地方会用到零拷贝技术

1、java的NIO

先说java,是因为要给下面的netty做铺垫,在 Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间(user space)中的用户缓冲区(user buffer)。

堆外内存(DirectBuffer)在使用后需要应用程序手动回收,而堆内存(HeapBuffer)的数据在 GC 时可能会被自动回收。因此,在使用 HeapBuffer 读写数据时,为了避免缓冲区数据因为 GC 而丢失,NIO 会先把 HeapBuffer 内部的数据拷贝到一个临时的 DirectBuffer 中的本地内存(native memory),这个拷贝涉及到 sun.misc.Unsafe.copyMemory() 的调用,背后的实现原理与 memcpy() 类似。 最后,将临时生成的 DirectBuffer 内部的数据的内存地址传给 I/O 调用函数,这样就避免了再去访问 Java 对象处理 I/O 读写。

(1)MappedByteBuffer

MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,意思是把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。这样添加地址映射,而不进行拷贝。

(2)DirectByteBuffer

DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,是 MappedByteBuffer 的具体实现类。因此同样具有零拷贝技术。

(3)FileChannel

FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。

我们直接看Linux2.4的版本,socket缓冲区做了调整,DMA带收集功能。

(1)DMA从拷贝至内核缓冲区

(2)将数据的位置和长度的信息的描述符增加至内核空间(socket缓冲区)

(3)DMA将数据从内核拷贝至协议引擎

这个复制过程是零拷贝过程。

2、Netty

Netty 中的零拷贝和上面提到的操作系统层面上的零拷贝不太一样, 我们所说的 Netty 零拷贝完全是基于(Java 层面)用户态的。

(1)Netty 通过 DefaultFileRegion 类对FileChannel 的 tranferTo() 方法进行包装,相当于是间接的通过java进行零拷贝。

(2)我们的数据传输一般都是通过TCP/IP协议实现的,在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,只有当这些数据包组成一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。

此时零拷贝的作用范围仅在用户空间中。那Netty是如何实现的呢?为此我们就要找到Netty进行数据传输的接口,这个接口一定包含了可以实现零拷贝的功能,这个接口就是ChannelBuffer。

既然有接口肯定就有实现类,一个最主要的实现类是CompositeChannelBuffer,这个类的主要作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作

为什么说是虚拟的呢,因为CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。

(3)ByteBuf 可以通过 wrap 操作把字节数组、ByteBuf、ByteBuffer 包装成一个 ByteBuf 对象, 进而避免了拷贝操作

(4)ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝

3、kafka

Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。

如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值