什么是内存零拷贝
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。
关键术语
内核态和用户态
内核态:内核空间是提供进程调度、内存分配、连接硬件资源等功能,是被系统保护的空间。进程运行于内核空间就叫做内核态
用户态:用户空间就算提供给各个进程使用的空间,不具备访问内核空间资源的权限,要访问内核空间,需要通过系统调度来完成。进程运行于用户空间就叫做用户态
上下文切换
上下文:cpu寄存器是cpu内置的容量小、速度极快的内存。程序计数器是存储cpu正在执行的指令位置。他们都是cpu在运行任务前,必须准备好的依赖环境,因此(cpu寄存器的内容和程序计数器的内容)叫做cpu上下文。
上下文切换:先把上一个任务的cpu上下文(cpu寄存器的内容和程序计数器)保存起来,然后加载新任务的上下文到cpu寄存器和程序计数器上,再跳转到程序计数器所指向的位置继续执行任务(现在一般指的是内核态和用户态的切换)
DMA
DMA全称Direct Memory Access,即直接内存访问。
DMA本质是主板的一个独立芯片,允许外部设备和内存存储器进行直接的IO传输,其过程不需要CPU的参与。
也就是说DMA在工作时,cpu是空闲的,可以去干其他事情,从而提升整体的效率
虚拟内存
虚拟地址取代物理地址,把硬盘当内存
优点:
- 虚拟内存空间可以远远大于 物理内存空间
- 多个虚拟内存可以指向同一个物理地址
零拷贝可以利用多个虚拟内存可以指向同一个物理地址这个特点,把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样就会减少IO的数据拷贝次数
传统IO
- 应用程序调用read函数,向操作系统发起IO调用,上下文:用户态->内核态
- DMA控制器把数据从磁盘中读取到内核缓冲区
- CPU把内核缓冲区数据拷贝到用户应用缓冲区,read函数返回,上下文:内核态->用户态
- 用户应用进程通过write函数,发起IO调用,上下文:用户态->内核态
- CPU将缓冲区的数据拷贝到socket缓冲区
- DMA控制器将数据从socket缓冲区拷贝到网卡设备,wirte函数返回,上下文:内核态->用户态
传统IO总共经历了4次上下文切换,4次拷贝(2次CPU拷贝和2次DMA拷贝)
实现零拷贝
零拷贝不是没有拷贝,而是减少用户态和内核态直接的切换以及cpu的拷贝次数
- mmap+write
- sendfile
- 带有DMA收集拷贝功能的sendfile
mmap
mmap用了虚拟内存的特点,将内核中的读缓冲区和用户空间的缓冲区进行映射,所有IO在内核中完成
- 用户进程通过调用mmap方法向操作系统内核发起IO调用,上下文:用户态->内核态
- cpu向DMA发起请求,DMA将数据从硬盘拷贝到内核缓冲区
- mmap方法返回,上下文:内核态->用户态
- 用户进程通过调用write方法向操作系统内核再次发起IO调用,上下文:用户态->内核态
- CPU将内核缓冲区的数据拷贝到socket缓冲区
- CPU向DMA发起请求,DMA将数据从socket缓冲区拷贝到网卡,wirte方法返回,上下文:内核态->用户态
mmap总共经历了4次上下文切换,3次拷贝(1次CPU拷贝和2次DMA拷贝)
sendfile
sendfile是linux2.1内核引入的一个系统调用函数,表示两个文件描述符直接传输数据,在操作系统的内核中操作,避免数据在内核缓冲区和用户缓冲区之间的拷贝,实现零拷贝操作
- 用户进程发起sendfile系统调用,上下文:用户态->内核态
- DMA控制器将数据从硬盘拷贝到内核缓冲区
- CPU将读缓存区中得数据拷贝到socket缓冲区
- DMA控制器异步将数据从socket缓冲区拷贝到网卡
- sendfile函数返回,上下文:内核态->用户态
sendfile总共经历了2次上下文切换以及3次拷贝(2次DMA拷贝+1次CPU拷贝)
sendfile+DMA scatter/gather
linux2.4版本后,对sendfile进行优化,引入SG-DMA,运用scatter/gather操作,他可以直接从内核空间缓冲区将数据读取到网卡,省去cpu拷贝到socket缓冲区
- 用户进程发起sendfile系统调用,上下文:用户态->内核态
- DMA控制器将数据从硬盘拷贝到内核缓冲区
- CPU把内核缓冲区中得文件描述符信息(包括内核缓冲区得内存地址和偏移量)直接发送到socket缓冲区
- DMA控制器根据文件描述符信息直接把数据从内核缓冲区拷贝到网卡
-
- sendfile函数返回,上下文:内核态->用户态
sendfile+DMA scatter/gather总共经历了2次上下文切换以及2次拷贝(2次DMA拷贝)(真.零拷贝),全程没有cpu参与拷贝数据
总结
实现方式有下面几种
1. mmap
2. sendfile
3. 升级版sendfile(DMA scatter/gather)
在java中,mmap最经典就是NIO用MappedByteBuffer进行实现内存映射
sendfile可以用FileChannel中得transferTo()或者transferFrom(),底层就是用sendfile(),kafka实现零拷贝也是用sendfile
附加
kafka零拷贝
kafka底层就是调用sendfile,拷贝就是零拷贝,所以快
netty零拷贝
- netty使用butebuffer(采用directbuffer),使用堆外直接内存进行socket读写,不需要进行字节缓冲区得二次拷贝
- netty使用Composite Buffers(组合buffer),能够聚合多个bytebuffer对象(保存多个buffer得引用,避免拷贝数据),将对各小buffer合并成大buffer
- 文件传输采用transferTo得方法(底层是sendfile)
是不是零拷贝最好
答案是:不是
PageCache
关键的技术是PageCache(磁盘高速缓存,内核缓冲区),PageCache的功能
- 缓存最近被访问的数据
- 预读能力(局部性原理)
如果大文件也用PageCache,那PageCache就会被大文件占据,导致热点小文件无法利用,反而降低性能。PageCache的功能也大受影响,所以大文件拷贝应该绕过PageCache。
直接IO
应用程序直接访问磁盘,直接绕过PageCache(内核缓冲区),减少一次内核缓冲区到应用程序的复制
异步IO
主要是解决进程阻塞问题,异步IO没有涉及PageCache
分情况
所以对于文件传输要分情况:
- 传输大文件时,使用异步IO+直接IO(使得大文件可以非阻塞进行操作)
- 传输小文件时,使用零拷贝技术