概念
- 零拷贝是指计算机执行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控制器)
- 用户进程调用read方法,向操作系统发出I/O请求,请求数据到自己的用户缓冲区中,进程进入阻塞状态;
- 操作系统收到请求后,进一步将I/O 请求发送DMA,然后让CPU执行其他任务;
- DMA进一步将I/O请求发送给磁盘;
- 磁盘收到DMA的I/O请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,让DMA发起中断信号,告知自己缓冲区已满;
- DMA收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到PageCache中,此时不占用CPU,CPU可以执行其他任务;
- 当DMA读取足够多的数据,就会发送中断信号给CPU;
- 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(); } }