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 中。过程大致如下:

  1. 创建一个 pipe 用来存储要复制的数据的元信息。

  2. 调用 splice 将 src fd 要复制数据的元信息写入到管道中。

  3. 调用 splice 从 pipe 中获取要复制数据的元信息,然后将数据复制到 dst fd 中。

../_images/splice.png

代码示例:https://github.com/chanfung032/labs/blob/master/splice.c

另外,pipe 这个可以通过池化来减少一次系统调用,增加性能。详细见:https://strikefreedom.top/pipe-pool-for-splice-in-go

参考&延伸: