IO系列-零拷贝原理

什么是零拷贝

零拷贝字面上的意思包括两个,“零”和“拷贝”:

  • “拷贝”:就是指数据从一个存储区域转移到另一个存储区域。
  • “零” :表示次数为0,它表示拷贝数据的次数为0。

合起来,那零拷贝就是不需要将数据从一个存储区域复制到另一个存储区域。

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

传统IO执行流程

做服务端开发的小伙伴,文件下载功能应该实现过不少了吧。如果你实现的是一个web程序,前端请求过来,服务端的任务就是:将服务端主机磁盘中的文件从已连接的socket发出去。关键实现代码如下:

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

传统的IO流程,包括read和write的过程。

  • read:把数据从磁盘读取到内核缓冲区,再拷贝到用户缓冲区
  • write:先把数据写入到socket缓冲区,最后写入网卡设备。

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

从流程图中可以看出,传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(2次CPU拷贝和2次DMA拷贝),什么是DMA拷贝?让我们一起来回顾下,零拷贝涉及到的操作系统知识点。

零拷贝相关的知识点

 内核空间和用户空间

我们电脑上跑着的应用程序,其实是需要经过操作系统,才能做一些特殊操作,如磁盘文件读写、内存的读写等。因为这些操作都是比较危险的操作,不可以由应用程序乱来,只能交给底层的操作系统来。
因此,操作系统为每个进程都分配了内存空间,一部分是用户空间,一部分是内核空间,内核空间是操作系统内核访问的区域,是受保护的内存空间,而用户空间是用户应用程序访问的内存区域。以32位操作系统为例,他会为每一个进程都分配了4G(2^32)的内存空间。

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

什么是用户态、内核态

  • 进程运行于内核空间,被称为进程的内核态
  • 进程运行在用户空间,被称为进程的用户态

什么是上下文切换

  • 什么是CPU上下文?

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

  • 什么是CPU上下文切换?

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

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

CPU寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。

虚拟内存 

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

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

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

DMA技术 

DMA,英文全称Direct Memory Access,即直接内存访问。DMA本质上市一块主板上独立的芯片,允许外设设备和内存存储器之间直接进行IO数据传输,其过程不需要CPU的参与。 我们一起来看下IO流程,DMA做了什么事情。

  • 用户应用进程调用read函数,向操作系统发起IO调用,进入阻塞状态,等待数据返回。
  • CPU收到指令后,对DMA控制器发起指令调度。
  • DMA收到IO请求后,将请求发送给磁盘
  • 磁盘将数据放入到磁盘控制缓冲区,并通知DMA
  • DMA将数据从磁盘控制缓冲区拷贝到内核缓冲区
  • DMA向CPU发起数据读完的信号,把工作交换给CPU,由CPU负责将数据从内核缓冲区拷贝到用户缓冲区
  • 用户应用进程由内核态切换用户态,接触阻塞状态

可以发现,DMA做的事情很清晰,它主要是帮忙CPU转发一下IO请求,以及拷贝数据。 为什么需要它?

主要就是效率,它帮忙CPU做事情,这时候,CPU就可以闲下来做别的事情,提高了CPU的利用率。

零拷贝实现的几种方式

零拷贝并不是没有拷贝,而是减少用户态/内核态的切换次数以及CPU拷贝的次数。零拷贝实现有多种方式,分别是

  • mmap+write
  • senfile
  • 带有DMA手机拷贝功能的senfile

mmap+write实现零拷贝

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实现的零拷贝流程如下:

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

可以发现,mmap+write实现的零拷贝,I/O发生了4次用户空间和内核空间的上下文切换,以及3次数据拷贝,包括了2次DMA拷贝和1次CPU拷贝。
mmap是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所有节省了一次CPU拷贝,并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。

senfile实现的零拷贝

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

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

  • out_fd:为待写入内容的文件描述符,一个socket描述符
  • in_fd:为待读出内容的文件描述符,必须是真实的文件,不能是socket和管道
  • offset:指定从读入文件的哪个位置开始读,如果是NULL,表示文件的默认起始位置。
  • count:指定fdout 和fdin之间传输的字节数

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

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

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

sendfile+DMA scatter/gather实现的零拷贝

linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝
sendfile+DMA scatter/gather 实现的零拷贝流程如下:

 

  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来进行传输的。

java 零拷贝方式

  • java NIO对mmap的支持
  • java NIO对sendfile的支持

Java NIO对mmap的支持

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

堆外内存

jvm堆空间中数据要发送到内核缓冲区,通常需要先将jvm堆空间中的数据拷贝到系统内存(一个非官方的理解,用C语言实现的本地方法调用中,首先需要将堆空间中的数据拷贝到C语言相关的存储结构),故提高性能的措施:使用堆外内存。 不过堆外内存中的数据,通常还是需要从堆空间中获取,从这个角度看,貌似提升性能有限。

内存映射(mmap+write)

从4.1分析mmap+write内存映射中可以发现,内存映射的核心思想就是将内核缓冲区、用户空间缓冲区映射到同一个物理地址上,可以减少用户缓冲区与内核缓冲区之间的数据拷贝。但并不会减少上下文切换次数

例子

public class MmapTest {

    public static void main(String[] args) {
   
        // 分配堆外内存
        // ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        try {

            FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
            //数据传输
            writeChannel.write(data);
            readChannel.close();
            writeChannel.close();

        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

 Java NIO对sendfile的支持

FileChannel 的 transferTo()/transferFrom(),底层就是sendfile()系统调用函数。kafka这个开源项目就是使用它,org.apache.kafka.common.network.PlaintextTransportLayer.transferFrom()方法,如下:

public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
    return fileChannel.transferTo(position, count, socketChannel);
}

 sendfile的demo如下:

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值