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
- https://zhuanlan.zhihu.com/p/381788230
- https://colobu.com/2022/11/19/zero-copy-and-how-to-use-it-in-go/
- https://colobu.com/2022/11/21/zero-copy-and-how-to-use-it-in-go-2/
- https://strikefreedom.top/archives/pipe-pool-for-splice-in-go