Go语言中零拷贝的原理与实现详解

 更新时间:2023年08月16日 08:50:51   作者:洋芋编程  
零拷贝是相对于用户态来讲的,即数据在用户态不发生任何拷贝,那么零拷贝的原理是什么,又是如何实现的呢,下面小编就来和大家详细聊聊吧

传统读写模式

传统读写模式流程图

  • 第一次数据拷贝: 用户进程发起 read() 系统调用,当前上下文从用户态切换至内核态,DMA(Direct Memory Access) 引擎从文件中读取数据,并存储到内核态缓冲区 (DMA 拷贝)
  • 第二次数据拷贝: 将数据从内核态缓冲区拷贝到用户态缓冲区 (CPU 拷贝),然后返回给用户进程,拷贝数据时会发生一次上下文切换 (从内核态切换到用户态)
  • 第三次数据拷贝: 用户进程发起 write() 系统调用,当前上下文从用户态切换至内核态,数据从用户态缓冲区被拷贝到 Socket 缓冲区 (CPU 拷贝)
  • 第四次数据拷贝: write() 系统调用结束返回到用户进程,当前上下文从内核态切换至用户态,第四次数据拷贝为异步执行,从 Socket 缓冲区拷贝到网卡 (DMA 拷贝)

transferTo

transferTo() 和 send() 类似,也是一个系统调用,用于在文件之间高效地传输数据。

transferTo 在操作系统层面实现了零拷贝技术,允许将数据直接从一个文件传输到另一个文件,而无需通过用户空间进行中转。

transferTo 流程图

  • 第一次数据拷贝: 用户进程发起 transferTo() 调用,将文件数据拷贝到一个 Read buffer(内核态)中,当前上下文从用户态切换至内核态
  • 第二次数据拷贝: 内核将 Read buffer 中的数据拷贝到 Socket 缓冲区
  • 第三次数据拷贝: 数据从 Socket 缓冲区拷贝到网卡,当前上下文从内核态切换至用户态

相比较于传统的读写模式, transferTo 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 3 次, 虽然已经前进了一大步,但是作为过渡阶段,transferTo 距离零拷贝还有一些距离。

零拷贝

零拷贝是相对于用户态来讲的,数据在用户态不发生任何拷贝。

sendfile + DMA

sendfile() 是作用于两个文件描述符之间的数据拷贝的系统调用,这个拷贝操作是直接在内核中进行的,没有用户态到内核态的数据拷贝和上下文切换带来的开销,所以称为零拷贝技术。

Linux2.4 内核对 sendfile 系统调用做了改进:

sendfile 改进

  • 用户进程发起 sendfile() 系统调用,当前上下文从用户态切换至内核态,DMA 将数据拷贝到内核缓冲区
  • 向 Socket 缓冲区中发送当前数据在内核缓冲区的地址和偏移量两个值
  • 根据 Socket 缓冲区的地址和偏移量,直接将内核缓冲区的数据拷贝到网卡,当前上下文从内核态切换至用户态

零拷贝流程图

相比较于传统的读写模式, sendfile + DMA 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 2 次 (2 次均为 DMA 拷贝),完全消除了数据从用户态和内核态之间拷贝数据带来的开销。

sendfile + DMA 虽然已经足够高效,但是依然存在两个不足之处:

  • 方案本身需要引入新的硬件支持
  • 输入文件描述符仅支持文件类型

splice

针对 sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系统调用, splice() 不需要硬件支持,能够实现在任意的两个文件描述符时之间传输数据。

splice() 是基于管道缓冲区机制实现的,所以两个参数文件描述符必须有一个是管道设备。在实际开发中,splice() 作为实现零拷贝的首选,因此 sendfile() 的内部实现也替换为了 splice()。

Go 语言中的零拷贝

现在有了前文的理论基础后,我们来看下在 Go 语言中标准库的零拷贝方法原型和应用方法,笔者的 Go 版本为 go1.19 linux/amd64

sendfile

sendfile 的方法原型为 syscall.Sendfile,文件路径为 syscall/syscall_unix.go。

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

一个简单的使用示例:

package main
import (
 "fmt"
 "os"
 "syscall"
)
func main() {
 // 设置源文件
 src, err := os.Open("/tmp/source.txt")
 if err != nil {
  panic(err)
 }
 defer src.Close()
 // 设置目标文件
 target, err := os.Create("/tmp/target.txt")
 if err != nil {
  panic(err)
 }
 defer target.Close()
 // 获取源文件的文件描述符
 srcFd := int(src.Fd())
 // 获取目标文件的文件描述符
 targetFd := int(target.Fd())
 // 使用 Sendfile 实现零拷贝 (拷贝 10 个字节)
 // 如果因为字符编码导致的字符截断问题 (如中文乱码问题), 结果自动保留到截断前的最后完整字节
 // 例如文件内容为 “星期三四五六七”,count 参数为 4, 那么只会拷贝第一个字 (一个汉字 3 个字节)
 // 但是需要注意的是,方法的返回值 written 不受影响 (和 count 参数保持一致)
 // 所以实际开发中,第三个参数 offset 必须设置正确,否则就可能引起乱码或数据丢失问题
 n, err := syscall.Sendfile(targetFd, srcFd, nil, 4)
 if err != nil {
  fmt.Println(err)
  return
 }
 fmt.Printf("写入字节数: %d", n)
}

splice

splice 的方法原型为 syscall.Splice,文件路径为 syscall/zsyscall_linux_amd64.go。

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

一个简单的使用示例:

package main
import (
 "fmt"
 "os"
 "syscall"
)
func main() {
 // 设置源文件
 src, err := os.Open("/tmp/source.txt")
 if err != nil {
  panic(err)
 }
 defer src.Close()
 // 设置目标文件
 target, err := os.Create("/tmp/target.txt")
 if err != nil {
  panic(err)
 }
 defer target.Close()
 // 创建管道文件
 // 作为两个文件传输数据的中介
 pipeReader, pipeWriter, err := os.Pipe()
 if err != nil {
  panic(err)
 }
 defer pipeReader.Close()
 defer pipeWriter.Close()
 // 设置文件读写模式
 // 笔者在标准库中没有找到对应的常量说明
 // 读者可以参考这个文档:
 //   https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants
 //   SPLICE_F_NONBLOCK = 0x2
 spliceNonBlock := 0x02
 // 使用 Splice 将数据从源文件描述符移动到管道 writer
 _, err = syscall.Splice(int(src.Fd()), nil, int(pipeWriter.Fd()), nil, 1024, spliceNonBlock)
 if err != nil {
  panic(err)
 }
 // 使用 Splice 将数据从管道 reader 移动到目标文件描述符
 n, err := syscall.Splice(int(pipeReader.Fd()), nil, int(target.Fd()), nil, 1024, spliceNonBlock)
 if err != nil {
  panic(err)
 }
 fmt.Printf("写入字节数: %d", n)
}

以上就是Go语言中零拷贝的原理与实现详解的详细内容,更多关于Go零拷贝的资料请关注脚本之家其它相关文章!

相关文章

  • 使用Golang Validator包实现数据验证详解

    使用Golang Validator包实现数据验证详解

    在开发过程中,数据验证是一个非常重要的环节,而golang中的Validator包是一个非常常用和强大的数据验证工具,提供了简单易用的API和丰富的验证规则,下面我们就来看看Validator包的具体使用吧
    2023-12-12
  • Go语言异常处理案例解析

    Go语言异常处理案例解析

    这篇文章主要介绍了Go语言异常处理案例解析,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • Go语言基础设计模式之策略模式示例详解

    Go语言基础设计模式之策略模式示例详解

    这篇文章主要为大家介绍了Go语言基础设计模式之策略模式示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2021-11-11
  • Go中Writer和Reader接口的使用入门

    Go中Writer和Reader接口的使用入门

    本文主要介绍了Go中Writer和Reader接口的使用入门,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • go等待一组协程结束的操作方式

    go等待一组协程结束的操作方式

    这篇文章主要介绍了go等待一组协程结束的操作方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 一文详解golang延时任务的实现

    一文详解golang延时任务的实现

    这篇文章主要为大家介绍了golang延时任务的实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Go语言编程实现支持六种级别的日志库 

    Go语言编程实现支持六种级别的日志库 

    这篇文章主要为大家介绍了使用Golang编写一个支持六种级别的日志库示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • 使用Golang简单实现七牛图片处理API

    使用Golang简单实现七牛图片处理API

    本文给大家实现的是使用Golang简单实现七牛图片处理API的方法和步骤,基于PIPE库实现的,非常的实用,有需要的小伙伴可以参考下
    2016-08-08
  • go语言实现顺序存储的栈

    go语言实现顺序存储的栈

    这篇文章主要介绍了go语言实现顺序存储的栈,实例分析了Go语言实现顺序存储的栈的原理与各种常见的操作技巧,需要的朋友可以参考下
    2015-03-03
  • golang新手们容易犯的3个错误总结

    golang新手们容易犯的3个错误总结

    这篇文章主要给大家介绍了关于golang新手们容易犯的3个错误,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-08-08

最新评论