详解Golang中零拷贝的原理以及实践

 更新时间:2023年07月02日 14:15:06   作者:蓝胖子的编程梦  
零拷贝技术相信大家都有所耳闻,但是本文不仅会讲述零拷贝技术的原理,并将从实际代码出发,看看零拷贝技术在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的调用将面临用户态和内核态缓冲区之间数据的拷贝。

整个拷贝过程如上图所示,磁盘和内核间的数据传递可以通过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映射成了文件的地址。示意图如下:

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

sendfile

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

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

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

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

splice

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

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中零拷贝的原理以及实践的文章就介绍到这了,更多相关Golang零拷贝内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang基于websocket实现的简易聊天室程序

    golang基于websocket实现的简易聊天室程序

    这篇文章主要介绍了golang基于websocket实现的简易聊天室,分析了websocket的下载、安装及使用实现聊天室功能的相关技巧,需要的朋友可以参考下
    2016-07-07
  • Go语言学习之条件语句使用详解

    Go语言学习之条件语句使用详解

    这篇文章主要介绍了Go语言中条件语句的使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • Go语言编译原理之源码调试

    Go语言编译原理之源码调试

    这篇文章主要为大家介绍了Go语言编译原理之源码调试示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Go 阻塞的实现示例

    Go 阻塞的实现示例

    Go语言提供了多种同步和通信机制,可以用于实现阻塞的效果,本文主要介绍了Go 阻塞的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2024-05-05
  • golang结构化日志slog的用法简介

    golang结构化日志slog的用法简介

    日志是任何软件的重要组成部分,Go 提供了一个内置日志包(slog),在本文中,小编将简单介绍一下slog包的功能以及如何在 Go 应用程序中使用它,感兴趣的可以了解下
    2023-09-09
  • Golang 的defer执行规则说明

    Golang 的defer执行规则说明

    这篇文章主要介绍了Golang 的defer执行规则说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • golang构建工具Makefile使用详解

    golang构建工具Makefile使用详解

    这篇文章主要为大家介绍了golang构建工具Makefile的使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • Go之interface的具体使用

    Go之interface的具体使用

    这篇文章主要介绍了Go之interface的具体使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-03-03
  • Go语言中slice作为参数传递时遇到的一些“坑”

    Go语言中slice作为参数传递时遇到的一些“坑”

    这篇文章主要给大家介绍了关于Go语言中slice作为参数传递时遇到的一些“坑”,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • 使用Go语言实现找出两个大文件中相同的记录

    使用Go语言实现找出两个大文件中相同的记录

    这篇文章主要为大家详细介绍了使用Go语言实现找出两个大文件中相同的记录的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-10-10

最新评论