零拷贝
Posted 烟草的香味
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了零拷贝相关的知识,希望对你有一定的参考价值。
是什么
什么是零拷贝呢? 这个词想必听过不止一次了吧, 但一直没有认真的研究一下这到底是个什么玩意.
在很久之前, 一次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
中使用了零拷贝的技术.
如何, 简单看下来, 零拷贝也没有那么什么嘛. 此项暂时搁置, 再见
理解了零拷贝原理,总结一下
为什么要零拷贝?
传统的IO拷贝在计算机中拷贝次数太多,速度太慢,零拷贝可以减少拷贝次数,增加系统性能。另外,零拷贝并不是指没有进行文件的拷贝,只是减少了拷贝的次数。
1、基本概念
1.1、 DMA控制器
首先需要知道计算机硬件的读写速度,大概如下:
- CPU高速缓存属于速度最快的,这里可以认为是飞机的速度。
- 网卡的速度,可以认为是汽车速度。
- 硬盘可以认为是走路的速度。
那假如把硬盘的文件通过网卡发送出去,普通IO,CPU高速缓存要向网卡写数据,即使CPU的速度再快,也会被其他两个设备影响。因此CPU需要一个小弟DMA控制器。
直接内存访问(DMA)是一种完全由硬件执行I/O交换的工作方式。在这种方式中,DMA控制器从CPU完全接管对总线的控制,数据交换不经过CPU,而直接在内存和I/O设备之间进行 。DMA方式一般用于高速传送成组数据。DMA控制器将向内存发出地址和控制信号,修改地址,对传送的字的个数计数,并且以中断方式向CPU报告传送操作的结束。
DMA方式的主要优点是速度快。由于CPU根本不参加传送操作,因此就省去了CPU取指令、取数、送数等操作。在数据传送过程中,没有保存现场、恢复现场之类的工作。内存地址修改、传送字个数的计数等等,也不是由软件实现,而是用硬件线路直接实现的。所以DMA方式能满足高速I/O设备的要求,也有利于CPU效率的发挥。
1.2、 CPU用户态和内核态
世界上的人本来就是不平等的,这就好比,你老婆能管你的钱,但是你只能闷头挣钱一样。程序也是如此,有的程序权限很高,可以访问计算机的任何资源,但是有的程序权限就低,只能访问部分资源。这两个类型的程序,就可以映射为CPU的用户态和内核态。方便记忆可以这么理解,内核态是计算机的核心,可以访问计算机的任何资源,如网卡、硬盘。但是为了安全,CPU不能让用户程序肆无忌惮的访问计算机的任何资源,这样如果用户程序不稳定可能会造成系统崩溃,因此才有的用户态。
- 内核态:cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
- 用户态:只能受限的访问内存,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
因此,在CPU想要读取硬盘文件的时候,需要从用户态切换为内核态,才有权限。读取完成之后,为了程序安全,需要从内核态切换为用户态。
2、零拷贝
2.1、普通拷贝
在普通的拷贝时,大概流程如下
- cpu切换到内核态,先到内核态查询内核缓冲区,如果内核缓冲区有,则可以直接拷贝到用户空间中。
- 如果内核缓冲区没有,则CPU会让DMA加载到内核空间中。这里就会有一次DMA拷贝。
- 拷贝到内核缓冲区之后,CPU将会从内核缓冲区拷贝走。这是一次CPU拷贝。拷贝完成切换到用户态。
- 写的时候,再次切换到内核态。切换完成之后,写到socket缓冲区。写完之后,切换到用户态。
- DMA通过异步的方式将socket缓冲区的数据通过网卡发送到对端。
总结这种普通的IO。总共有4次CPU切换(上图蓝色)分别是读2次、写2次。4次文件拷贝,分别是:
- 文件从硬盘到内核空间
- 内核空间到CPU
- CPU到socket缓冲区
- socket缓冲区到网卡。
2.2、mmap零拷贝
mmap是零拷贝的一种方式通过虚拟内存的方式实现。也就是说用户空间和内核空间使用同一个物理地址。这样,文件就不在需要经过用户空间。可以从内核缓冲区直接复制到socket缓冲区。减少了一次文件拷贝。
2.3、sendfile零拷贝
sendfile函数在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,被称为零拷贝
- 系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到内核缓冲区,然后数据被内核直接拷贝到另外一个与 socket 相关的 socket缓冲区。这里没有 用户态和核心态 之间的切换,在内核中直接完成了从一个 缓冲区 到另一个缓冲区的拷贝。
- DMA 把数据从内核缓冲区 直接拷贝给协议栈,没有切换,也不需要数据从用户态和核心态,因为数据就在 内核里。
总结
零拷贝并不是没有拷贝,是指减少拷贝的次数。有两种方式mmap和sendfile。
- mmap 适合小数据量读写,sendFile 适合大文件传输。(这个并没有查到详细理论依据,如果您有线索,欢迎留言)
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
如果有误,欢迎指正。
以上是关于零拷贝的主要内容,如果未能解决你的问题,请参考以下文章