零拷貝技術第二篇: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 的封裝。

因爲直接調用系統調用很方便,所以很多時候我們可以模仿標準庫實現我們自己零拷貝的方法。所以個人感覺這些傳統的方式沒有太多錦上添花的東西可做了,要做的就是新的零拷貝系統接口的封裝或者自定義開發。

參考資料

[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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rmtUELGqaVxZFrjzeqSYlQ