零拷貝技術第二篇: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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rmtUELGqaVxZFrjzeqSYlQ