transferTo() 和 send() 类似,也是一个系统调用,用于在文件之间高效地传输数据。
transferTo 在操作系统层面实现了零拷贝技术,允许将数据直接从一个文件传输到另一个文件,而无需通过用户空间进行中转。
相比较于传统的读写模式, transferTo 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 3 次, 虽然已经前进了一大步,但是作为过渡阶段,transferTo 距离零拷贝还有一些距离。
零拷贝是相对于用户态来讲的,数据在用户态不发生任何拷贝。
sendfile() 是作用于两个文件描述符之间的数据拷贝的系统调用,这个拷贝操作是直接在内核中进行的,没有用户态到内核态的数据拷贝和上下文切换带来的开销,所以称为零拷贝技术。
Linux2.4 内核对 sendfile 系统调用做了改进:
相比较于传统的读写模式, sendfile + DMA 把上下文的切换次数从 4 次减少到 2 次,同时把数据拷贝的次数从 4 次降低到了 2 次 (2 次均为 DMA 拷贝),完全消除了数据从用户态和内核态之间拷贝数据带来的开销。
sendfile + DMA 虽然已经足够高效,但是依然存在两个不足之处:
针对 sendfile + DMA 方案存在的不足,Linux 引入了 splice() 系统调用, splice() 不需要硬件支持,能够实现在任意的两个文件描述符时之间传输数据。
splice() 是基于管道缓冲区机制实现的,所以两个参数文件描述符必须有一个是管道设备。在实际开发中,splice() 作为实现零拷贝的首选,因此 sendfile() 的内部实现也替换为了 splice()。
现在有了前文的理论基础后,我们来看下在 Go 语言中标准库的零拷贝方法原型和应用方法,笔者的 Go 版本为 go1.19 linux/amd64
。
sendfile 的方法原型为 syscall.Sendfile,文件路径为 syscall/syscall_unix.go。
func Sendfile(outfd int, infd int, offset *int64, count int) (written int, err error)
一个简单的使用示例:
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
// 设置源文件
src, err := os.Open("/tmp/source.txt")
if err != nil {
panic(err)
}
defer src.Close()
// 设置目标文件
target, err := os.Create("/tmp/target.txt")
if err != nil {
panic(err)
}
defer target.Close()
// 获取源文件的文件描述符
srcFd := int(src.Fd())
// 获取目标文件的文件描述符
targetFd := int(target.Fd())
// 使用 Sendfile 实现零拷贝 (拷贝 10 个字节)
// 如果因为字符编码导致的字符截断问题 (如中文乱码问题), 结果自动保留到截断前的最后完整字节
// 例如文件内容为 “星期三四五六七”,count 参数为 4, 那么只会拷贝第一个字 (一个汉字 3 个字节)
// 但是需要注意的是,方法的返回值 written 不受影响 (和 count 参数保持一致)
// 所以实际开发中,第三个参数 offset 必须设置正确,否则就可能引起乱码或数据丢失问题
n, err := syscall.Sendfile(targetFd, srcFd, nil, 4)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("写入字节数: %d", n)
}
splice 的方法原型为 syscall.Splice,文件路径为 syscall/zsyscall_linux_amd64.go。
func Splice(rfd int, roff *int64, wfd int, woff *int64, len int, flags int) (n int64, err error)
一个简单的使用示例:
package main
import (
"fmt"
"os"
"syscall"
)
func main() {
// 设置源文件
src, err := os.Open("/tmp/source.txt")
if err != nil {
panic(err)
}
defer src.Close()
// 设置目标文件
target, err := os.Create("/tmp/target.txt")
if err != nil {
panic(err)
}
defer target.Close()
// 创建管道文件
// 作为两个文件传输数据的中介
pipeReader, pipeWriter, err := os.Pipe()
if err != nil {
panic(err)
}
defer pipeReader.Close()
defer pipeWriter.Close()
// 设置文件读写模式
// 笔者在标准库中没有找到对应的常量说明
// 读者可以参考这个文档:
// https://pkg.go.dev/golang.org/x/sys/unix#pkg-constants
// SPLICE_F_NONBLOCK = 0x2
spliceNonBlock := 0x02
// 使用 Splice 将数据从源文件描述符移动到管道 writer
_, err = syscall.Splice(int(src.Fd()), nil, int(pipeWriter.Fd()), nil, 1024, spliceNonBlock)
if err != nil {
panic(err)
}
// 使用 Splice 将数据从管道 reader 移动到目标文件描述符
n, err := syscall.Splice(int(pipeReader.Fd()), nil, int(target.Fd()), nil, 1024, spliceNonBlock)
if err != nil {
panic(err)
}
fmt.Printf("写入字节数: %d", n)
}
Why Kafka Is so Fast: https://medium.com/swlh/why-kafka-is-so-fast-bde0d987cd03
[2]Efficient data transfer through zero copy: https://developer.ibm.com/articles/j-zerocopy/
[3]Optimizing Large File Transfers in Linux with Go — An Exploration of TCP and Syscall: https://itnext.io/optimizing-large-file-transfers-in-linux-with-go-an-exploration-of-tcp-and-syscall-ebe1b93fb72f
[4]directio: https://github.com/ncw/directio
[5]Go 语言中的零拷贝优化: https://strikefreedom.top/archives/pipe-pool-for-splice-in-go
[6]Linux I/O 原理和 Zero-copy 技术全面揭秘: https://strikefreedom.top/archives/linux-io-and-zero-copy#toc-head-15
[7]零拷贝技术第一篇:综述: https://colobu.com/2022/11/19/zero-copy-and-how-to-use-it-in-go/
[8]direct io: https://github.com/cch123/golang-notes/blob/master/io.md
[9]sys/unix: https://pkg.go.dev/golang.org/x/sys/unix#Splice
[10]sendfile: https://github.com/hslam/sendfile
[11]splice: https://github.com/hslam/splice
[12]zerocopy: https://github.com/acln0/zerocopy