Go 语言实现——io.Copy 和 zerocopy 优化¶
io.Copy
系函数对文件和文件之间、文件和 socket 之间的复制做了不少 zerocopy 优化。
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rt, ok := dst.(ReaderFrom); ok {
return rt.ReadFrom(src)
}
// 非 zerocopy 版 copyBuffer
// ...
}
如果 src 实现了 WriterTo、 dst 实现了 ReaderFrom 接口,就会走到 zerocopy 路径。dst Writer 实现 ReaderFrom 接口在标准库中见的比较多,以 *TcpConn 为例:
// ReadFrom implements the io.ReaderFrom ReadFrom method.
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
}
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)
}
TcpConn 会尝试使用 splice 或者 sendfile 等 zerocopy 方法来复制数据,在这两个方法都无法从 src 复制数据的时候,才 fallback 回使用普通的用户空间复制数据方法。
下面是封装的 splice 函数,splice 函数会判断 src Reader 是不是 *TcpConn ,是的话才会调用 splice 调用。同理 sendfile 也会做类型判断,只有对的类型才会做 zerocopy 优化。
func splice(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)
if ok {
remain, r = lr.N, lr.R
if remain <= 0 {
return 0, nil, true
}
}
var s *netFD
if tc, ok := r.(*TCPConn); ok {
s = tc.fd
} else if uc, ok := r.(*UnixConn); ok {
if uc.fd.net != "unix" {
return 0, nil, false
}
s = uc.fd
} else {
return 0, nil, false
}
written, handled, sc, err := poll.Splice(&c.pfd, &s.pfd, remain)
if lr != nil {
lr.N -= written
}
return written, wrapSyscallError(sc, err), handled
}
完整的代码见:https://go-review.googlesource.com/c/go/+/107715
splice 这个系统调用以前没见过,这个系统调用可以用来将一个 fd 的数据在内核中直接复制到另一个 fd 中。过程大致如下:
创建一个 pipe 用来存储要复制的数据的元信息。
调用 splice 将 src fd 要复制数据的元信息写入到管道中。
调用 splice 从 pipe 中获取要复制数据的元信息,然后将数据复制到 dst fd 中。
代码示例:https://github.com/chanfung032/labs/blob/master/splice.c
另外,pipe 这个可以通过池化来减少一次系统调用,增加性能。详细见:https://strikefreedom.top/pipe-pool-for-splice-in-go
参考&延伸: