零拷贝
是什么
什么是零拷贝呢? 这个词想必听过不止一次了吧, 但一直没有认真的研究一下这到底是个什么玩意.
在很久之前, 一次IO 操作的流程大致是这样的:
假设, 这里的 IO 设备是磁盘, 那么磁盘的一次read
操作流程如下:
- CPU向磁盘发起 IO 请求
- 磁盘将数据放入磁盘控制器缓冲区(上图步骤1), 并发起 IO 中断通知 CPU
- CPU 将数据拷贝到 Pagecache 中 (上图步骤2)
- 再将数据从 Pagecache 拷贝到应用缓冲区中 (上图步骤3)
read
函数返回, 应用读取到文件内容 (上图步骤4)
在上面的数据获取的过程中, 发生了3次数据的拷贝, 其中2次是 CPU 全程参与的. 而这个过程, CPU 忙于拷贝数据, 无暇做其他工作.
为了减轻 CPU 的压力, DMA
应运而生.
DMA
做的事情, 简单说来就是上图步骤2. 数据从 IO 设备缓冲区到内核缓冲区的拷贝工作, 不需要CPU 参与, 也就腾出一定的时间来做其他事情了. 其操作步骤大致如下:
- 应用程序调用
read
方法发起 IO 请求 - 系统向
DMA
发起 IO 请求, 然后继续处理其他工作 DMA
向磁盘发起 IO 请求- 磁盘收到请求后, 将数据拷贝到磁盘缓冲区, 通过中断通知
DMA
DMA
将数据拷贝到内核缓冲区(Pagecache), 然后通知 CPU 读取- CPU 再将数据拷贝到应用缓冲区并返回
read
于是, 一次数据读取的流程变成了这样:
现在, 每一次的数据读取, 都会发生2次数据拷贝(IO设备内部的就不算在其中了). 而零拷贝就是为了解决这个问题.
解决方案
mmap
想一下, 为什么数据需要从内核缓冲区拷贝到应用缓冲区? 他们用的明明是同一个物理内存呀. 还不是因为虚拟内存的存在, 所以他们在内存空间的不同地址. 如果能够让他俩共用用一段物理内存, 不就不需要拷贝数据了.
mmap
的本意, 是将磁盘文件的内容直接映射到一段内存空间中进行读取, 而这恰好也减少了数据的拷贝.
在Go
中使用mmap
如下:
package main
import (
"fmt"
"golang.org/x/exp/mmap"
)
func main() {
at, _ := mmap.Open("./tmp.txt")
buff := make([]byte, 1024)
_, _ = at.ReadAt(buff, 0)
_ = at.Close()
fmt.Println(string(buff))
}
但是, 遗憾的是, 官方包没有提供write
方法. 好在有一些优秀的开源项目可供参考.
mmap
很好的解决了数据拷贝带来的消耗, 虽然还有一次DMS
负责的数据拷贝, 但DMS
不会影响 CPU 的执行.
单独读取一个设备, 亦或者单独向一个设备中写入内容, 这样确认很好, 但是, 如果我们要进行文件传输, 将一个文件的内容发送到网卡, 那么这个流程在使用mmap
时就是这样的:
sendfile
我们知道, 应用程序调用系统是需要进行上下文切换的, 是否有一个函数直接告诉 CPU 把2个 IO 设备的数据进行拷贝? 这样就可以减少一次系统调用嘛.
没错, 使用的, 通过sendfile
的方式, 整个流程大致如下:
在Go
中可直接通过函数syscall.Sendfile
实现.
你以为这就完了么? 不, 这还不是零拷贝, 这此种仍然存在一次 CPU 主导的内存拷贝.
零拷贝
借用之前mmap
的思路, 既然应用程序和内核可以公用同一个缓冲区, 网卡和磁盘为什么不可以呢? 于是, 复制流程就成了下面这样:
网卡和 IO 设备使用同一个内存地址空间. 在这个过程中所有的数据拷贝均有DMA
参与, CPU没有参与, 极大的提升了传输效率.
注意, 此功能需要 linux2.4 以上, 且网卡支持(通过命令ethtool -k eth0 | grep scatter-gather
查看)才行.
而这, 就是常说的零拷贝技术了. 零拷贝不是真的没有发生数据拷贝, 而是CPU
没有负责数据拷贝.
如何使用零拷贝呢? 还是调用sendfile
, 如果在支持零拷贝的系统上, 就会自动使用零拷贝技术啦.
总结
已知的, 在kafka
中使用了零拷贝的技术.
如何, 简单看下来, 零拷贝也没有那么什么嘛. 此项暂时搁置, 再见