io.copy 在 linux 下仅当源为 os.file(os.o_rdonly 打开)、目标为未包装的 net.tcpconn、连接 fd 有效且内核 ≥2.6.33 时才真正调用 sendfile 系统调用。

Go 里大文件传输想用 sendfile 零拷贝,只在 Linux 上可行,且必须满足三个硬性条件:源是 *os.File、目标是裸 *net.TCPConn、连接未被任何中间层包装(比如 TLS、HTTP/2、gzip 中间件)。不满足任一条件,io.Copy 或 file.WriteTo 都会自动降级为用户态拷贝。
什么时候 io.Copy 会真正调用 sendfile 系统调用?
io.Copy 在 Linux 下会尝试触发 sendfile,但仅当:
- 源参数是
*os.File(且以os.O_RDONLY打开;/proc、pipe、bytes.Buffer全部不认) - 目标参数是未包装的
net.Conn,且底层能 cast 成*net.TCPConn(http.ResponseWriter、bufio.Writer、tls.Conn都不行) - 目标连接 fd 有效,且未被 hijack 后 close 过(否则
conn.(*net.TCPConn).File()会提前关闭 fd) - 内核版本 ≥ 2.6.33,socket 建议设为非阻塞(避免卡住 M 线程)
验证是否生效:运行 strace -e trace=sendfile,read,write ./your-binary。只看到 sendfile 调用,没混入 read/write,才算成功。
file.WriteTo(conn) 是唯一安全的零拷贝入口,但限制极多
*os.File.WriteTo 是标准库中唯一明确设计为“可能走 sendfile”的接口,但它不是开关,而是带 fallback 的条件分支:
- 它内部先尝试
syscall.Sendfile,失败则 fallback 到io.Copy - 传入的
conn必须实现io.WriterTo接口,而只有裸*net.TCPConn实现了它;http.ResponseWriter没有实现,所以http.ServeFile永远不走零拷贝 - 哪怕你手写
file.WriteTo(conn),若conn是crypto/tls.Conn或被http.Hijack后再封装,也会立刻退化
示例片段:
f, _ := os.Open("big.log")
conn, _ := net.Dial("tcp", "127.0.0.1:8080")
// ✅ 可能触发 sendfile
f.WriteTo(conn)
手动调用 syscall.Sendfile 容易翻车的点
手动调用绕过标准库封装,自由度高但风险集中,常见错误包括:
-
offset是int64值传递,不会自动更新;分段传输必须自己维护偏移量,漏了就重复发或跳段 - 源 fd 必须是普通文件(
S_ISREG),os.Pipe、/proc/sys/net/core/somaxconn这类伪文件会返回ESPIPE - 目标 fd 必须是 socket(且不能是监听 socket),传错类型直接
EINVAL - 必须先用
conn.(*net.TCPConn).SyscallConn().Fd()提取原始 fd,漏掉SyscallConn()会 panic 或传入无效值 - 遇到
EAGAIN(发送缓冲区满)不能忽略,需配合非阻塞 + 轮询,否则阻塞主线程
典型调用签名:
nr, err := syscall.Sendfile(int(outfd), int(infd), offset, count)注意参数顺序和 errno 处理逻辑,别直接套用 C 版本原型。
真正难的不是调用 sendfile,而是确保整个数据链路全程不引入任何用户态中介——从文件打开方式、连接获取路径、到中间件堆叠顺序,任何一个环节加一层封装,零拷贝就失效。这使得它在真实 HTTP 服务中几乎只适用于裸 TCP 文件分发场景,而非 Web 服务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











