零拷贝简介

是什么

什么是零拷贝呢? 这个词想必听过不止一次了吧, 但一直没有认真的研究一下这到底是个什么玩意.

在很久之前, 一次IO 操作的流程大致是这样的:

零拷贝简介_第1张图片

假设, 这里的 IO 设备是磁盘, 那么磁盘的一次read操作流程如下:

  1. CPU向磁盘发起 IO 请求
  2. 磁盘将数据放入磁盘控制器缓冲区(上图步骤1), 并发起 IO 中断通知 CPU
  3. CPU 将数据拷贝到 Pagecache 中 (上图步骤2)
  4. 再将数据从 Pagecache 拷贝到应用缓冲区中 (上图步骤3)
  5. read函数返回, 应用读取到文件内容 (上图步骤4)

在上面的数据获取的过程中, 发生了3次数据的拷贝, 其中2次是 CPU 全程参与的. 而这个过程, CPU 忙于拷贝数据, 无暇做其他工作.

为了减轻 CPU 的压力, DMA应运而生.

DMA做的事情, 简单说来就是上图步骤2. 数据从 IO 设备缓冲区到内核缓冲区的拷贝工作, 不需要CPU 参与, 也就腾出一定的时间来做其他事情了. 其操作步骤大致如下:

  1. 应用程序调用read方法发起 IO 请求
  2. 系统向DMA发起 IO 请求, 然后继续处理其他工作
  3. DMA向磁盘发起 IO 请求
  4. 磁盘收到请求后, 将数据拷贝到磁盘缓冲区, 通过中断通知DMA
  5. DMA将数据拷贝到内核缓冲区(Pagecache), 然后通知 CPU 读取
  6. CPU 再将数据拷贝到应用缓冲区并返回read

于是, 一次数据读取的流程变成了这样:

零拷贝简介_第2张图片

现在, 每一次的数据读取, 都会发生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时就是这样的:

零拷贝简介_第3张图片

sendfile

我们知道, 应用程序调用系统是需要进行上下文切换的, 是否有一个函数直接告诉 CPU 把2个 IO 设备的数据进行拷贝? 这样就可以减少一次系统调用嘛.

没错, 使用的, 通过sendfile的方式, 整个流程大致如下:

零拷贝简介_第4张图片

Go中可直接通过函数syscall.Sendfile实现.

你以为这就完了么? 不, 这还不是零拷贝, 这此种仍然存在一次 CPU 主导的内存拷贝.

零拷贝

借用之前mmap的思路, 既然应用程序和内核可以公用同一个缓冲区, 网卡和磁盘为什么不可以呢? 于是, 复制流程就成了下面这样:

零拷贝简介_第5张图片

网卡和 IO 设备使用同一个内存地址空间. 在这个过程中所有的数据拷贝均有DMA参与, CPU没有参与, 极大的提升了传输效率.

注意, 此功能需要 linux2.4 以上, 且网卡支持(通过命令ethtool -k eth0 | grep scatter-gather查看)才行.

而这, 就是常说的零拷贝技术了. 零拷贝不是真的没有发生数据拷贝, 而是CPU没有负责数据拷贝.

如何使用零拷贝呢? 还是调用sendfile, 如果在支持零拷贝的系统上, 就会自动使用零拷贝技术啦.

总结

已知的, 在kafka中使用了零拷贝的技术.

如何, 简单看下来, 零拷贝也没有那么什么嘛. 此项暂时搁置, 再见


原文地址: https://hujingnb.com/archives/896

你可能感兴趣的:(linux)