零拷贝原理以及实践

标签: 零拷贝 原理 实践 | 发表时间:2023-06-25 17:21 | 作者:蓝胖子的编程梦
出处:https://juejin.cn/backend

大家好,我是蓝胖子,零拷贝技术相信大家都有所耳闻,但是今天呢,我不仅会讲述零拷贝技术的原理,并将从实际代码出发,看看零拷贝技术在golang中的应用。现在让我们开始吧。

零拷贝原理

零拷贝技术的原理本质上就是减少数据的拷贝次数,因为当调用传统read write方法读取文件内容并返回给客户端的时候,会经过四次拷贝。我用golang代码举例如下

  func main() {  
   http.HandleFunc("/tradition", func(writer http.ResponseWriter, request *http.Request) {  
  
      f, _ := os.Open("./testmmap.txt")  
      buf := make([]byte, 1024)  
      // 内核拷贝到buf
      n, _ := f.Read(buf)  
      // buf拷贝到内核
      writer.Write(buf[:n])  
   })  
   http.ListenAndServe(":8080", http.DefaultServeMux)  
}

如上面代码所示,如果我们需要将本地testmmap.txt文件的内容读出来返回给客户端。

testmmap.txt里只有一个hello的单词,当服务启动以后访问接口便会返回hello。

  (base) ➜  codelearning git:(master) ✗ cat testmmap.txt
hello
(base) ➜  codelearning git:(master) ✗ curl localhost:8080/tradition
hello

整个过程需要经过read和write两次系统调用,而每次read和write的调用将面临用户态和内核态缓冲区之间数据的拷贝。

image.png

整个拷贝过程如上图所示,磁盘和内核间的数据传递可以通过DMA技术让cpu不参与其中,但是内核态和用户态间的数据拷贝则需要经过cpu参与,涉及到了两次系统调用,和4次数据拷贝。

mmap+write

基于上述传统文件的访问方式,我们可以用mmap技术进行优化,mmap可以让用户缓冲区buf的地址和文件磁盘地址建立映射,这样访问用户缓冲区buf的数据就等效于访问磁盘文件上的数据。

用mmap优化后的文件访问代码如下:

    
http.HandleFunc("/mmap", func(writer http.ResponseWriter, request *http.Request) {  
   f, _ := os.Open("./testmmap.txt")  
   data, err := syscall.Mmap(int(f.Fd()), 0, 5, syscall.PROT_READ, syscall.MAP_SHARED)  
   if err != nil {  
      panic(err)  
   }  
   writer.Write(data)  
})

可以看到mmap返回了一个data的字节数组,这个字节数组的内容就是映射了文件内容,之后将字节数组写入到响应体里。

syscall.Mmap(int(f.Fd()), 0, 5, syscall.PROT_READ, syscall.MAP_SHARED)

这里再解释下mmap涉及的参数含义:

其中第一个参数代表要映射的文件描述符。

接着是映射的范围是从0个字节到第5个字节。

第四个参数 代表映射的后的内存区域是只读的,类似的参数还有 syscall.PROT_WRITE表示内存区域可以被写入,syscall.PROT_NONE表示内存区域不可访问。

第五个参数表示 映射的内存区域可以被多个进程共享,这样一个进程修改了这个内存区域的数据,对其他进程是可见的,并且修改后的内容会自动被操作系统同步到磁盘文件里。

类似的参数还有syscall.MAP_PRIVATE表示内存区域是私有的,不可被其他进程访问,声明为私有后,每个进程拥有单独的一份内存映射拷贝,并且对此内存区域进行修改不会被同步到磁盘文件。

注意整个过程,我们是没有将文件内容读取到用户空间的任何缓冲区的。我们仅仅是在write系统调用时,告诉了内核一个地址(即字节数组的地址),而这个地址被mmap映射成了文件的地址。示意图如下:

image.png

整个过程是用户进程告诉内核需要拷贝的数据数据的地址,然后内核拷贝数据。

sendfile

基于上述mmap+write方式进行优化后的文件内容访问减少了一次拷贝过程,不过系统调用还是两次。如果用sendfile的话可以将系统调用减少到一次。

  func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error) 

Sendfile的系统调用可以将目的文件描述符和源文件描述符传递进去,剩下的拷贝过程就交给内核了。示意图如下:

image.png

但是sendfile对源文件描述符有要求,普通的文件可以,如果源文件描述符是socket则不能用sendfile了。

splice

splice系统调用则是为了解决源文件描述符和目的文件描述符都是socket的情况而产生的。splice系统调用的原理是通过管道让数据在源socket和目的socket之间进行传输。示意图如下:

image.png

splice的系统调用方法如下:

  func Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error) 

注意splice系统调用需要保证传入的文件描述符,rfd或者wfd至少一个是管道的文件描述符。创建管道也是一个系统调用,如下:

  func Pipe2(p []int, flags int) error 

再回到通过splice系统调用的情况,可以看到要调用两次splice系统调用,才能完成socket间的数据传递,因为splice系统调用会根据源文件描述符或目的文件描述符是管道的情况做不同的动作。

第一次系统调用,目的文件描述符是管道,那么内核则会将管道和源文件描述符绑定在一起,注意此时是不会进行数据拷贝的。

第二次splice系统调用,源文件描述符是管道,那么内核才会将管道内的数据拷贝到目的文件描述符,由于在前一次,管道已经和源文件描述符进行了绑定,所以这次的splice系统调用,实际上会将源文件描述符的数据拷贝到目的文件描述符。

整个过程,抛开DMA技术拷贝的次数,一共只有一次数据拷贝的过程。

零拷贝在golang中的实践

讲完了零拷贝涉及的技术,我们来看看golang是如何运用这些技术的。拿一个比较常用的方法举例,io.Copy, 其底层调用了copyBuffer方法,copyBuffer会判断copy的目的接口Writer是否实现了ReaderFrom 接口,如果实现了则直接调用ReaderFrom 从src读取数据。

  func Copy(dst Writer, src Reader) (written int64, err error) {  
   return copyBuffer(dst, src, nil)  
}

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {  
   // If the reader has a WriteTo method, use it to do the copy.  
   // Avoids an allocation and a copy.   if wt, ok := src.(WriterTo); ok {  
      return wt.WriteTo(dst)  
   }  
   // Similarly, if the writer has a ReadFrom method, use it to do the copy.  
   if rt, ok := dst.(ReaderFrom); ok {  
      return rt.ReadFrom(src)  
   }  
   // 进行传统的文件读取,代码较长,暂时省略了。
   .......
   return written, err  
}

net.TcpConn实现了ReadFrom 接口,拿net.TcpConn举例,看看它的实现。

  func (c *TCPConn) readFrom(r io.Reader) (int64, error) {  
   if n, err, handled := splice(c.fd, r); handled {  
      return n, err  
   }  
   if n, err, handled := sendFile(c.fd, r); handled {  
      return n, err  
   }  
   return genericReadFrom(c, r)  
}

最终net.TcpConn 会调用readFrom方法从来源io.Reader读取数据,而readFrom读取数据用到的技术则是刚刚所讲的零拷贝技术,这里用到了splice和sendFile系统调用,如果来源io.Reader是一个tcp连接或者时unix 连接则会调用splice进行数据拷贝,否则就会调用sendFile进行数据拷贝,具体细节我就不在这里展开了。

总之,你可以看到,其实我们平时用到的方法就用到了零拷贝技术,这些经常说的底层原理离我们并不遥远,学习,永远怀着一颗谦卑的心。

相关 [零拷贝 原理 实践] 推荐:

零拷贝原理以及实践

- - 掘金 后端
大家好,我是蓝胖子,零拷贝技术相信大家都有所耳闻,但是今天呢,我不仅会讲述零拷贝技术的原理,并将从实际代码出发,看看零拷贝技术在golang中的应用. 零拷贝技术的原理本质上就是减少数据的拷贝次数,因为当调用传统read write方法读取文件内容并返回给客户端的时候,会经过四次拷贝. 如上面代码所示,如果我们需要将本地testmmap.txt文件的内容读出来返回给客户端.

Linux 中的零拷贝技术,第 2 部分

- _li_ming_ - IBM developerWorks 中国 : 文档库
本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景. 第一部分主要介绍了一些零拷贝技术的相关背景知识,简要概述了 Linux 为什么需要零拷贝技术以及 Linux 中都有哪几种零拷贝技术. 本文是本系列文章的第二部分,针对第一部分内容中提到的几种零拷贝技术分别进行更详细的介绍,并对这些零拷贝技术的优缺点进行分析.

Linux 中的零拷贝技术,第 1 部分

- _li_ming_ - IBM developerWorks 中国 : 文档库
本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景. 本文是本系列文章的第一部分,主要是介绍一些零拷贝技术的相关背景知识,简要概述了 Linux 为什么需要零拷贝技术以及 Linux 中都有哪几种零拷贝技术.

Flutter原理与实践

- - 美团点评技术团队
Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件. 自从2017年5月发布 第一个版本以来,目前Flutter已经发布了近60个版本,并且在2018年5月发布了第一个 “Ready for Production Apps”的Beta 3版本,6月20日发布了第一个 “Release Preview”版本.

React Native 原理与实践

- - 掘金 前端
React Native 介绍. 什么是 React Native. React Native 是一个由 Facebook 于 2015 年 9 月发布的一款开源的 JavaScript 框架,它可以让开发者使用 JavaScript 和 React 来开发跨平台的移动应用. 它既保留了 React 的开发效率,又同时拥有 Native 应用的良好体验,加上 Virtual DOM 跨平台的优势,实现了真正意义上的:.

程序分析-原理和实践

- 三十不归 - 弯曲评论
今年秋天在UCSB旁听一门Program Analysis的课(课程主页:http://www.cs.ucsb.edu/~benh/cs290/),觉得Ben的课程风格很实在,从头到尾没有口水话,几乎是干货. 回想之前对Program Analysis感兴趣却常常找不到合适的资料,而这个技术其实在很多方面都比较有用,因此想把这门课程记录下来的笔记陆续发出来,如果有朋友感觉有用,就没有白费一番介绍的力气了.

HBase 原理、设计与优化实践

- - leejun_2005的个人页面
HBase —— Hadoop Database的简称,Google BigTable的另一种开源实现方式,从问世之初,就为了解决用大量廉价的机器高速存取海量数据、实现数据分布式存储提供可靠的方案. 从功能上来讲,HBase不折不扣是一个数据库,与我们熟悉的Oracle、MySQL、MSSQL等一样,对外提供数据的存储和读取服务.

Presto实现原理和美团的使用实践

- - 美团技术团队
Facebook的数据仓库存储在少量大型Hadoop/HDFS集群. Hive是Facebook在几年前专为Hadoop打造的一款数据仓库工具. 在以前,Facebook的科学家和分析师一直依靠Hive来做数据分析. 但Hive使用MapReduce作为底层计算框架,是专为批处理设计的. 但随着数据越来越多,使用Hive进行一个简单的数据查询可能要花费几分到几小时,显然不能满足交互式查询的需求.

Lucene实践:全文检索的基本原理

- - 开源软件 - ITeye博客
http://lucene.apache.org/java/docs/index.html 定义:. 是一个高效的,基于Java. 所以在了解Lucene之前要费一番工夫了解一下全文检索. 我们生活中的数据总体分为两种:. 结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等. 非结构化数据: 指不定长或无固定格式的数据,如邮件,word文档等.