详解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内存管理之内存分配器详解

    Golang内存管理之内存分配器详解

    Go内存分配器的设计思想来源于TCMalloc,全称是Thread-Caching Malloc,核心思想是把内存分为多级管理,下面就来和大家深入聊聊Go语言内存分配器的使用吧
    2023-06-06
  • Go 语言简单实现Vigenere加密算法

    Go 语言简单实现Vigenere加密算法

    这篇文章主要介绍了Go语言简单实现Vigenere加密算法,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下
    2022-09-09
  • Golang Defer关键字特定操作详解

    Golang Defer关键字特定操作详解

    defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源,这篇文章主要介绍了golang中的defer函数理解,需要的朋友可以参考下
    2023-03-03
  • go语言标准库fmt包的一键入门

    go语言标准库fmt包的一键入门

    这篇文章主要为大家介绍了go语言标准库fmt包的一键入门使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • goland Duration 和time的区别说明

    goland Duration 和time的区别说明

    这篇文章主要介绍了goland Duration 和time的区别说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go 1.21.0 新增结构化日志记录标准库log/slog使用详解

    Go 1.21.0 新增结构化日志记录标准库log/slog使用详解

    这篇文章主要为大家介绍了Go 1.21.0 新增结构化日志记录标准库log/slog使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11
  • Golang开发使用gorm时打印SQL语句方式

    Golang开发使用gorm时打印SQL语句方式

    这篇文章主要介绍了Golang开发使用gorm时打印SQL语句方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • Go 不支持 []T转换为[]interface类型详解

    Go 不支持 []T转换为[]interface类型详解

    这篇文章主要为大家介绍了Go不支持[]T转换为[]interface类型详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • Golang标准库之errors包应用方式

    Golang标准库之errors包应用方式

    Go语言的errors包提供了基础的错误处理能力,允许通过errors.New创建自定义error对象,error在Go中是一个接口,通过实现Error方法来定义错误文本,对错误的比较通常基于对象地址,而非文本内容,因此即使两个错误文本相同
    2024-10-10
  • Go与Rust高性能解析JSON实现方法示例

    Go与Rust高性能解析JSON实现方法示例

    这篇文章主要为大家介绍了Go与Rust高性能的解析JSON实现方法示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12

最新评论