概念

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


过程

早期的I/O过程

  • CPU发出对应的指令给磁盘控制器;
  • 磁盘控制器收到指令后,开始准备数据并把数据放到磁盘控制器的内部缓冲区中,然后产生一个中断;
  • CPU收到中断信号后,将磁盘控制器的缓冲区的数据一次一个字节的读进自己的寄存器,然后把寄存器的数据写入到内存,(也就是将数据从磁盘控制器拷贝到PageCache,再将数据从PageCache拷贝到用户缓冲区)而在数据传输期间CPU是无法执行其他任务单。


前提掌握

DMA技术:
  • 直接访问内存技术;
  • 也就是在进行I/O设备和内存的数据传输的时候,数据搬运的工作全部交给DMA控制器,而CPU不在参与任何与数据搬运相关的事情,这样CPU就可以去处理别的事务
PageCache:
  • 需要将磁盘文件数据拷贝到内核缓冲区,这个内核缓冲区实际上是磁盘高速缓存(PageCache);
  • PageCache使用了预读功能,每次读取大小为4kb;
  • 缓存最近被访问的数据(当空间不足时,淘汰最久未被访问的缓存)。

传统的I/O过程

(也就是从磁盘控制器缓冲区中的数据拷贝到PageCache中,此时不占用CPU,而是使用DMA控制器)
  1. 用户进程调用read方法,向操作系统发出I/O请求,请求数据到自己的用户缓冲区中,进程进入阻塞状态;
  2. 操作系统收到请求后,进一步将I/O 请求发送DMA,然后让CPU执行其他任务;
  3. DMA进一步将I/O请求发送给磁盘;
  4. 磁盘收到DMA的I/O请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,让DMA发起中断信号,告知自己缓冲区已满;
  5. DMA收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到PageCache中,此时不占用CPU,CPU可以执行其他任务;
  6. 当DMA读取足够多的数据,就会发送中断信号给CPU;
  7. CPU收到DMA的信号,知道数据已经准备好,就将数据从内核拷贝到用户空间,系统调用返回。


传统I/O的工作方式

数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的I/O接口从磁盘读取或写入;
read、write
期间发生:
  • 发生两次系统调用(read、write),
  • 发生了四次用户态与内核态的上下文切换(都是先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态)
  • 发生了四次数据拷贝,两次是DMA拷贝(从磁盘控制器缓冲区到PageCache、socket缓冲区),两次是CPU拷贝(从内存缓冲区到用户缓冲区)



零拷贝

  • 因为我们没有在内存层面去拷贝数据,也就是说全程没有通过CPU搬运数据,所有数据都是通过DMA来进行传输的;
  • 只需要2次上下文切换和数据拷贝次,就可以完成文件的传输,并且2次数据拷贝由DMA搬运。

mmap + write

mmap()系统会调用函数直接把内核缓冲区的数据映射到用户空间,也就是系统内核与用户空间就不需要再进行任何的数据拷贝操作。

具体过程:
  • 应用进程调用mmap()后,DMA会把磁盘的数据拷贝到PageCache里面。接着应用进程跟操作系统内核共享这个缓冲区;
  • 应用进程再调用write(),操作系统直接将PageCache的数据拷贝到socket缓冲区中,这一切都发生在内核态,由CPU来搬运数据;
  • 最后,把内核的socket缓冲区的数据,拷贝到网卡的缓冲区里,这个过程是由DMA搬运的。

改变点:

  • 也就是mmap()读取的时候,应用进程与系统内核共享内存缓冲区的数据;
  • write()写入的时候,操作系统直接将内核的缓冲区的数据拷贝到socket缓冲区中,都发生在内核态,由CPU搬运。
根本来说:也就是少了一次数据拷贝的过程,但是仍是4次上下文切换(因为系统调用还是2次mmap、write)。

sendfile

由于sendfile一条命令代替read、write两个系统调用;也就是减少了一次系统调用,也就减少了2次上下文切换;

真正的零拷贝:如果网卡支持SG-DMA技术

过程:
  • 通过DMA将磁盘上的数据拷贝到PageCache中;
  • 缓冲区描述符合数据长度传入到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将PageCache中的数据拷贝到网卡的缓冲区;也就是不需要将数据拷贝到socket缓冲区,减少了一次CPU拷贝;
  • 此时只有两次拷贝并且都是DMA拷贝;


补充:

大文件传输:异步I/O + 直接I/O



零拷贝的应用

java提供的零拷贝的方式

Java NIO 对 mmap的支持,使用MappedByteBuffer的类

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());
        }
    }
}

Java NIO 对 sendfile的支持,使用transferTo() / transferFrom()的类

项目中的应用:文件的下载
/**
     *@Author hshuo
     *@Description 下载Excel导入模板
     * 将文件以流的形式一次性读取到内存,通过响应输出流输出到前端
     * 通过NIO里面的fileChannel实现零拷贝(底层使用sendfile)
     * 因为设置了响应对象 其在方法执行时已经开始输出所以无需设置方法返回值 同时该接口不能与@ResponseBody配合使用
     *@Date 2021/12/8
     *@Param [response]
     **/
    @RequestMapping("/downloadTemplate")
    public void downloadTemplate(HttpServletResponse response){
        try {
            String path = ResourceUtils.getURL("classpath:").getPath() + excelTemplateUrl + excelTemplateFileName;
            log.info("资产管理Excel导入模板下载路径为:{}", path);
            File file = new File(path);


            //设置响应头
            response.reset();
            //设置response的Header
            response.setCharacterEncoding("UTF-8");
            //Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存
            //attachment表示以附件方式下载   inline表示在线打开   "Content-Disposition: inline; filename=文件名.mp3"
            //filename表示文件的默认名称,因为网络传输只支持URL编码的相关支付,因此需要将文件名URL编码后进行传输,前端收到后需要反编码才能获取到真正的名称
            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(excelTemplateFileName, "UTF-8"));
            response.setContentType("application/octet-stream");


            //读到流中
            FileInputStream fileInputStream = new FileInputStream(file);
//            获取输出流通道
            OutputStream outputStream = response.getOutputStream();
            WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
            FileChannel fileChannel = fileInputStream.getChannel();

//            采用零拷贝的方式实现文件的下载
            fileChannel.transferTo(fileChannel.position(), fileChannel.size(), writableByteChannel);

//            关闭资源
            fileChannel.close();
            outputStream.flush();
            writableByteChannel.close();
            log.info("正在下载Excel导入模板文件");
        }catch (IOException e){
            e.printStackTrace();
        }
    }

参考: