什么是零拷贝呢? 这个词想必听过不止一次了吧, 但一直没有认真的研究一下这到底是个什么玩意.
在很久之前, 一次IO 操作的流程大致是这样的:
假设, 这里的 IO 设备是磁盘, 那么磁盘的一次read
操作流程如下:
read
函数返回, 应用读取到文件内容 (上图步骤4)在上面的数据获取的过程中, 发生了3次数据的拷贝, 其中2次是 CPU 全程参与的. 而这个过程, CPU 忙于拷贝数据, 无暇做其他工作.
为了减轻 CPU 的压力, DMA
应运而生.
DMA
做的事情, 简单说来就是上图步骤2. 数据从 IO 设备缓冲区到内核缓冲区的拷贝工作, 不需要CPU 参与, 也就腾出一定的时间来做其他事情了. 其操作步骤大致如下:
read
方法发起 IO 请求DMA
发起 IO 请求, 然后继续处理其他工作DMA
向磁盘发起 IO 请求DMA
DMA
将数据拷贝到内核缓冲区(Pagecache), 然后通知 CPU 读取read
于是, 一次数据读取的流程变成了这样:
现在, 每一次的数据读取, 都会发生2次数据拷贝(IO设备内部的就不算在其中了). 而零拷贝就是为了解决这个问题.
想一下, 为什么数据需要从内核缓冲区拷贝到应用缓冲区? 他们用的明明是同一个物理内存呀. 还不是因为虚拟内存的存在, 所以他们在内存空间的不同地址. 如果能够让他俩共用用一段物理内存, 不就不需要拷贝数据了.
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
时就是这样的:
我们知道, 应用程序调用系统是需要进行上下文切换的, 是否有一个函数直接告诉 CPU 把2个 IO 设备的数据进行拷贝? 这样就可以减少一次系统调用嘛.
没错, 使用的, 通过sendfile
的方式, 整个流程大致如下:
在Go
中可直接通过函数syscall.Sendfile
实现.
你以为这就完了么? 不, 这还不是零拷贝, 这此种仍然存在一次 CPU 主导的内存拷贝.
借用之前mmap
的思路, 既然应用程序和内核可以公用同一个缓冲区, 网卡和磁盘为什么不可以呢? 于是, 复制流程就成了下面这样:
网卡和 IO 设备使用同一个内存地址空间. 在这个过程中所有的数据拷贝均有DMA
参与, CPU没有参与, 极大的提升了传输效率.
注意, 此功能需要 linux2.4 以上, 且网卡支持(通过命令ethtool -k eth0 | grep scatter-gather
查看)才行.
而这, 就是常说的零拷贝技术了. 零拷贝不是真的没有发生数据拷贝, 而是CPU
没有负责数据拷贝.
如何使用零拷贝呢? 还是调用sendfile
, 如果在支持零拷贝的系统上, 就会自动使用零拷贝技术啦.
已知的, 在kafka
中使用了零拷贝的技术.
如何, 简单看下来, 零拷贝也没有那么什么嘛. 此项暂时搁置, 再见
原文地址: https://hujingnb.com/archives/896