浅析零拷贝
简介
零拷贝(零复制) 技术是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域
零拷贝通常用于 通过网络传输文件时节省CPU周期和内存带宽
作用
- 减少甚至完全避免不必要的 CPU 拷贝,从而让 CPU 解脱出来去执行其他的任务
- 减少内存带宽的占用
- 通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换
基础概念
用户空间 和 内核空间
在 Linux 操作系统中,除了引导系统的 BIN 区,整个内存空间主要被分成两个部分:内核空间 (Kernel space) 、 用户空间 (User space) ,用户空间和内核空间的 空间、操作权限以及作用都是不一样的
- 内核空间:Linux 自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等系统进程使用
- 用户空间:则是提供给各个应用进程的主要空间
用户空间不具有访问内核空间资源的权限,因此如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成
系统调用:由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口
用户空间和内核空间的上下文切换是耗时的,一个系统调用执行的过程如下(涉及两次上下文切换):
处于用户空间时调用系统调用,由用户态切换成内核态
内核态执行相应操作,操作完成后返回,由内核态切换回用户态
Linux I/O 读写方式——轮询、I/O 中断以及 DMA 传输这 3 种磁盘与主存之间的数据传输机制
轮询:基于死循环对 I/O 端口进行不断检测
I/O 中断:当数据到达时,磁盘主动向 CPU 发起中断请求,由 CPU 自身负责数据的传输过程
- DMA 传输在 I/O 中断的基础上引入了 DMA 磁盘控制器负责数据的传输,降低了 I/O 中断操作对 CPU 资源的大量消耗
DMA 允许外存将数据直接拷贝到主存储器中并且传输不需要 CPU 的参与,以此将CPU解放出来去完成其他的事情
而用户空间与内核空间之间的数据传输并没有类似 DMA 这种可以不需要 CPU 参与的传输工具,因此用户空间与内核空间之间的数据传输是需要 CPU 全程参与的
由此产生了 零拷贝——减少和避免不必要的CPU数据拷贝过程
传统 I/O 存在的问题
以下的内容基于应用场景——将系统中的文件发送到远端
在 Linux 系统中,传统的访问方式是通过 write()
和 read()
两个系统调用实现的
- 通过
read()
函数读取文件到到缓存区中 - 通过
write()
方法把缓存中的数据输出到网络端口
下图描述了传统 I/O 操作的数据读写流程
整个过程涉及 2 次 CPU 拷贝、2 次 DMA 拷贝总共 4 次拷贝,以及 4 次上下文切换,具体流程描述如下
发出
read
系统调用,用户空间到内核空间的上下文切换 (第一次上下文切换)通过 DMA 将文件从磁盘上读取到内核空间缓冲区 (第一次拷贝)
read
系统调用返回,将内核空间缓冲区的数据拷贝到用户空间缓冲区 (第二次拷贝)系统调用的返回又会导致一次内核空间到用户空间的上下文切换 (第二次上下文切换)
发出
write
系统调用,用户空间到内核空间的上下文切换 (第三次上下文切换)将用户空间缓冲区中的数据拷贝到内核空间中与
socket
相关联的缓冲区中 (第三次拷贝)write
系统调用返回,导致内核空间到用户空间的再次上下文切换 (第四次上下文切换)通过 DMA 将内核缓冲区中的数据传递到协议引擎(第四次拷贝)
问: 传统 I/O 模式为什么要先将数据从磁盘读取到内核空间缓冲区,然后再将数据从内核空间缓冲区拷贝到用户空间缓冲区?为什么不直接将数据从磁盘读取到用户空间缓冲区就好?
答:传统 I/O 模式之所以将数据从磁盘读取到内核空间缓冲区而不是直接读取到用户空间缓冲区,是为了减少磁盘 I/O 操作的次数以此来提高性能。操作系统会根据局部性原理在一次 read
系统调用的时候预读取更多的文件数据到内核空间缓冲区中,这样当下一次 read
系统调用的时候发现要读取的数据已经存在于内核空间缓冲区中的时候只要直接拷贝数据到用户空间缓冲区中即可,无需再进行一次低效的磁盘 I/O 操作。
解决
观察上述传统流程,发现第二、三两次 CPU 拷贝其实是可以避免的,即 通过零拷贝实现
在 Linux 中零拷贝技术主要有 3 个实现思路
- 用户态直接 I/O:应用程序直接访问硬件存储,操作系统内核只辅助数据传输,这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间,因此直接 I/O 避免了内核空间缓冲区和用户空间缓冲区之间的数据拷贝
- 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝,这也是当前主流零拷贝技术的实现思路
- 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作
根据实现思路,具体的实现方式如下
mmap
+ write
mmap
系统调用函数会直接将内核缓冲区里的数据映射到用户空间,操作系统内核与用户空间共享缓存区
通过 mmap
系统调用即可实现共享,从而减少一次 CUP 拷贝,具体的读写流程如下
此时整个过程涉及 1 次 CPU 拷贝、2 次 DMA 拷贝总共 3 次拷贝,以及 4 次上下文切换,相较于传统方式而言减少了一次 CPU 拷贝
具体流程描述如下
发出
mmap
系统调用,用户空间到内核空间的上下文切换 (第一次上下文切换)通过 DMA 将文件从磁盘上读取到内核空间缓冲区 (第一次拷贝)
mmap
系统调用返回,导致一次内核空间到用户空间的上下文切换 (第二次上下文切换)发出
write
系统调用,用户空间到内核空间的上下文切换 (第三次上下文切换)将用户空间缓冲区中的数据拷贝到内核空间中与
socket
相关联的缓冲区中 (第二次拷贝)write
系统调用返回,导致内核空间到用户空间的再次上下文切换 (第四次上下文切换)通过 DMA 将内核缓冲区中的数据传递到协议引擎(第三次拷贝)
sendfile
在 Linux 内核版本 2.1
中,提供了一个专门发送文件的系统调用函数 sendfile
它可以替代前面的 read()
和 write()
这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就减少了一次 CPU 拷贝
具体的读写流程如下
此时整个过程涉及 1 次 CPU 拷贝、2 次 DMA 拷贝总共 3 次拷贝,以及 2 次上下文切换,相较于传统方式而言优化了很多
具体流程描述如下
发出
sendfile
系统调用,用户空间到内核空间的上下文切换 (第一次上下文切换)通过 DMA 将文件从磁盘上读取到内核空间缓冲区 (第一次拷贝)
将数据从内核空间缓冲区拷贝到内核中与
socket
相关的缓冲区中 (第二次拷贝)sendfile
系统调用返回,导致内核空间到用户空间的再次上下文切换 (第二次上下文切换)通过 DMA 将内核缓冲区中的数据传递到协议引擎(第三次拷贝)
但是这还没有达到真正的零拷贝 (零 CPU 拷贝),如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),就可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket
缓冲区的过程
从 Linux 内核 2.4
版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile()
系统调用的过程发生了点变化,实现了零拷贝
具体读写流程如下
此时整个过程涉及 2 次 DMA 拷贝总共 2 次拷贝,以及 2 次上下文切换,达到了 零拷贝
具体流程描述如下
发出
sendfile
系统调用,用户空间到内核空间的上下文切换 (第一次上下文切换)通过 DMA 将文件从磁盘上读取到内核空间缓冲区 (第一次拷贝)
将相应的描述符信息会被拷贝到相应的
socket
缓冲区当中,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里 (第二次拷贝),此过程不需要将数据从操作系统内核缓冲区拷贝到socket
缓冲区中,这样就减少了一次数据拷贝sendfile
系统调用返回,导致内核空间到用户空间的再次上下文切换 (第二次上下文切换)
总结
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运
总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上
参考博客:
【1】https://www.cnblogs.com/rickiyang/p/13265043.html