零拷贝技术第二篇:Go语言中的应用

书接上回:零拷贝技术第一篇:综述[1], 我们留了一个小尾巴,还没有介绍Go语言中零拷贝技术的应用,那么本文将带你了解Go标准库中零拷贝技术。

Go标准库中的零拷贝

在Go标准库中,也广泛使用了零拷贝技术来提高性能。因为零拷贝相关的技术很多都是通过系统调用提供的,所以在Go标准库中,也封装了这些系统调用,相关封装的代码可以在internal/poll[2]找到。

我们以Linux为例,毕竟我们大部分的业务都是在Linux运行的。

sendfile

internal/poll/sendfile_linux.go文件中,封装了sendfile系统调用,我删除了一部分的代码,这样更容易看到它是如何封装的:

/ SendFile wraps the sendfile system call.
func SendFile(dstFD *FD, src int, remain int64) (int64, error) {
 ...... //写锁

 dst := dstFD.Sysfd
 var written int64
 var err error
 for remain > 0 {
  n := maxSendfileSize
  if int64(n) > remain {
   n = int(remain)
  }
  n, err1 := syscall.Sendfile(dst, src, nil, n)
  if n > 0 {
   written += int64(n)
   remain -= int64(n)
  } else if n == 0 && err1 == nil {
   break
  }
  ...... // error处理
 }
 return written, err
}

可以看到SendFile调用senfile批量写入数据。sendfile系统调用一次最多会传输 0x7ffff00(2147479552) 字节的数据。这里Go语言设置maxSendfileSize为 0<<20 (4194304)字节。

net/sendfile_linux.go文件中会使用到它:

func sendFile(c *netFD, r io.Reader) (written int64, err error, handled bool) {
 var remain int64 = 1 << 62 // by default, copy until EOF

 lr, ok := r.(*io.LimitedReader)
 ......
 f, ok := r.(*os.File)
 if !ok {
  return 0, nil, false
 }

 sc, err := f.SyscallConn()
 if err != nil {
  return 0, nil, false
 }

 var werr error
 err = sc.Read(func(fd uintptr) bool {
  written, werr = poll.SendFile(&c.pfd, int(fd), remain)
  return true
 })
 if err == nil {
  err = werr
 }

 if lr != nil {
  lr.N = remain - written
 }
 return written, wrapSyscallError("sendfile", err), written > 0
}

这个函数谁又会调用呢?是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)
}

这个方法又会被ReadFrom方法封装。记住这个ReadFrom方法,我们待会再说。

func (c *TCPConn) ReadFrom(r io.Reader) (int64, error) {
 if !c.ok() {
  return 0, syscall.EINVAL
 }
 n, err := c.readFrom(r)
 if err != nil && err != io.EOF {
  err = &OpError{Op: "readfrom", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
 }
 return n, err
}

TCPConn.readFrom方法实现很有意思。它首先检查是否满足使用splice系统调用进行零拷贝优化,在目的是TCP connection, 源是TCP或者是Unix connection才能调用splice。否则才尝试使用sendfile。如果要使用sendfile优化,也有限制,要求源是*os.File文件。再否则使用不同的拷贝方式。

ReadFrom又会在什么情况下被调用?实际上你经常会用到,io.Copy就会调用ReadFrom。也许在不经意之间,当你在将文件写入到socket过程中,就不经意使用到了零拷贝。当然这不是唯一的调用和被使用的方式。

如果我们看一个调用链,就会把脉络弄清楚:io.Copy -> *TCPConn.ReadFrom -> *TCPConn.readFrom -> net.sendFile -> poll.sendFile

splice

上面你也看到了,*TCPConn.readFrom初始就是尝试使用splice,使用的场景和限制也提到了。net.splice函数其实是调用poll.Splice:

func Splice(dst, src *FD, remain int64) (written int64, handled bool, sc string, err error) {
 p, sc, err := getPipe()
 if err != nil {
  return 0, false, sc, err
 }
 defer putPipe(p)
 var inPipe, n int
 for err == nil && remain > 0 {
  max := maxSpliceSize
  if int64(max) > remain {
   max = int(remain)
  }
  inPipe, err = spliceDrain(p.wfd, src, max)
  handled = handled || (err != syscall.EINVAL)
  if err != nil || inPipe == 0 {
   break
  }
  p.data += inPipe

  n, err = splicePump(dst, p.rfd, inPipe)
  if n > 0 {
   written += int64(n)
   remain -= int64(n)
   p.data -= n
  }
 }
 if err != nil {
  return written, handled, "splice", err
 }
 return written, true, "", nil
}

在上一篇中讲到pipe如果每次都创建其实挺损耗性能的,所以这里使用了pip pool,也提到是潘少优化的。

所以你看到,不经意间你就会用到splice或者sendfile。

CopyFileRange

copy_file_range_linux.go[3]封装了copy_file_range系统调用。因为这个系统调用非常的新,所以封装的时候首先要检查Linux的版本,看看是否支持此系统调用。版本检查和调用批量拷贝的代码我们略过,具体看是怎么使用这个系统调用的:

func copyFileRange(dst, src *FD, max int) (written int64, err error) {
 if err := dst.writeLock(); err != nil {
  return 0, err
 }
 defer dst.writeUnlock()
 if err := src.readLock(); err != nil {
  return 0, err
 }
 defer src.readUnlock()
 var n int
 for {
  n, err = unix.CopyFileRange(src.Sysfd, nil, dst.Sysfd, nil, max, 0)
  if err != syscall.EINTR {
   break
  }
 }
 return int64(n), err
}

哪里会使用到它呢?of.File的读取数据的时候:

var pollCopyFileRange = poll.CopyFileRange

func (f *File) readFrom(r io.Reader) (written int64, handled bool, err error) {
 // copy_file_range(2) does not support destinations opened with
 // O_APPEND, so don't even try.
 if f.appendMode {
  return 0, false, nil
 }

 remain := int64(1 << 62)

 lr, ok := r.(*io.LimitedReader)
 if ok {
  remain, r = lr.N, lr.R
  if remain <= 0 {
   return 0, true, nil
  }
 }

 src, ok := r.(*File)
 if !ok {
  return 0, false, nil
 }
 if src.checkValid("ReadFrom") != nil {
  // Avoid returning the error as we report handled as false,
  // leave further error handling as the responsibility of the caller.
  return 0, false, nil
 }

 written, handled, err = pollCopyFileRange(&f.pfd, &src.pfd, remain)
 if lr != nil {
  lr.N -= written
 }
 return written, handled, NewSyscallError("copy_file_range", err)
}

同样的是*FIle.ReadFrom调用:

func (f *File) ReadFrom(r io.Reader) (n int64, err error) {
 if err := f.checkValid("write"); err != nil {
  return 0, err
 }
 n, handled, e := f.readFrom(r)
 if !handled {
  return genericReadFrom(f, r) // without wrapping
 }
 return n, f.wrapErr("write", e)
}

所以这个优化用在文件的拷贝中,一般的调用链路是 io.Copy -> *File.ReadFrom -> *File.readFrom -> poll.CopyFileRange -> poll.copyFileRange

标准库零拷贝的应用

Go标准库将零拷贝技术在底层做了封装,所以很多时候你是不知道的。比如你实现了一个简单的文件服务器:

import "net/http"

func main() {
	// 绑定一个handler
	http.Handle("/", http.StripPrefix("/static/", http.FileServer(http.Dir("../root.img"))))
	// 监听服务
	http.ListenAndServe(":8972", nil)
}

调用链如左:http.FileServer -> *fileHandler.ServeHTTP -> http.serveFile -> http.serveContent -> io.CopyN -> io.Copy -> 和sendFile的调用链接上了。可以看到访问文件的时候是调用了sendFile。

第三方库

有几个库提供了sendFile/splice的封装。

  • https://github.com/acln0/zerocopy

  • https://github.com/hslam/splice

  • https://github.com/hslam/sendfile

因为直接调用系统调用很方便,所以很多时候我们可以模仿标准库实现我们自己零拷贝的方法。所以个人感觉这些传统的方式没有太多锦上添花的东西可做了,要做的就是新的零拷贝系统接口的封装或者自定义开发。

参考资料

[1]

零拷贝技术第一篇:综述: https://colobu.com/2022/11/19/zero-copy-and-how-to-use-it-in-go/

[2]

internal/poll: https://github.com/golang/go/tree/600db8a514600df0d3a11edc220ed7e2f51ca158/src/internal/poll

[3]

copy_file_range_linux.go: https://github.com/golang/go/blob/600db8a514600df0d3a11edc220ed7e2f51ca158/src/internal/poll/copy_file_range_linux.go

往期推荐

零拷贝技术第二篇:Go语言中的应用_第1张图片

零拷贝技术第一篇:综述

零拷贝技术第二篇:Go语言中的应用_第2张图片

2022 GopherChina大会紧急通知!

零拷贝技术第二篇:Go语言中的应用_第3张图片

Go中的HTTP资源泄露之谜

想要了解Go更多内容,欢迎扫描下方关注公众号,回复关键词 [实战群]  ,就有机会进群和我们进行交流

零拷贝技术第二篇:Go语言中的应用_第4张图片

分享、在看与点赞Go 零拷贝技术第二篇:Go语言中的应用_第5张图片

你可能感兴趣的:(golang,开发语言,后端)