【Golang】Golang 中的零拷贝

Posted by 西维蜀黍 on 2021-11-02, Last Modified on 2023-02-20

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。

标准库零拷贝的应用

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。

Reference