
技术概念
零复制(Zero-copy,也称零拷贝),是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
上面这段说明是百度百科的解释,我看了一下wiki的解释,一个字不差,不知道谁抄的谁的
但不管谁是原创,在业务应用上领域,眨眼看上去还是看不懂
但有一点读懂的是,他的应用场景主要是网络传输文件
零拷贝能解决什么问题
一个文件下载服务器
我们有一段代码程序,是一个web服务,主要作用是提供服务器上文件下载功能
当一个请求操作进来时,我们的计算机会执行一系列相关的IO操作,包含文件的读取、复制、传输等,示例如下
步骤1:磁盘数据 → 内核缓冲区(操作系统管理的内存)
步骤2:内核缓冲区 → 应用程序缓冲区(你的Go代码里的变量)
步骤3:应用程序缓冲区 → 套接字缓冲区(网络发送的内核内存)
步骤4:套接字缓冲区 → 网卡硬件
这里步骤 2 和步骤 3 就是冗余拷贝:数据只是从内核到应用程序 "过了一手",没做任何处理就又送回内核。零拷贝的核心就是去掉这些无意义的拷贝,减少 CPU 消耗和内存带宽占用。
简单来说,就是需要把文件读取到你的程序变量中,再进行网络传输,这里也就意味着,我们下载的每一个文件,都要过一遍程序的内存申请、释放
虽说内存在使用完,会释放掉,但如果能直接把服务器上的文件直接进行网络传输,岂不是节省了内存的申请释放过程
进行一下调整
如果我们把上面步骤2和步骤3省略掉,那程序里怎么实现读取文件,传输给客户端?
的确,代码还是要写的,文件不读取拷贝如何传递给客户端,难道手动拿U盘去服务器拷贝么?
零拷贝不是完全不拷贝(最终数据还是要到磁盘去取),而是避免应用程序参与数据拷贝。
实现思路是让内核直接处理数据转移,应用程序只需要告诉内核 “数据在哪”“要传到哪”(通过指针或描述符)。
直白的来说,就相当于程序之前是操作一个变量的值进行传递,现在修改为操作该变量的指针
我们先来看一下改进后的示例
步骤1:磁盘数据 → 内核缓冲区
步骤2:内核缓冲区 → 套接字缓冲区(内核内部直接转移,不经过应用程序)
步骤3:套接字缓冲区 → 网卡硬件
还有一种更极致的做法:内核直接把磁盘数据的内存地址 “告诉” 网卡,跳过套接字缓冲区(需要硬件支持,比如 DMA)。
在业务中的应用场景
简单说:当数据"只是路过"应用程序,不需要二次处理时,零拷贝收益最大。具体包括:
- 大文件传输:比如下载服务、视频流服务,数据从磁盘到网络,无需应用程序处理。
- 日志收集 / 转发:日志数据从文件或网络接收后,直接转发到其他地方(如 ELK),无需解析。
- 中间件数据传递:如消息队列、缓存系统,数据在内存中直接转发,减少拷贝开销。
在程序中的应用
这里提到的用户态,就把它当作代码程序的操作即可,或者说变量
在Go语言中,零拷贝技术的实现依赖于操作系统提供的底层系统调用(如sendfile、mmap等),注意哈,以来系统层的技术
Go标准库已经对这些系统调用的进行了封装,无需过多关心实现。
以下是Go中零拷贝的几个实用场景
sendfile 系统调用(最典型)
- 作用:直接在内核态将文件数据从文件描述符拷贝到套接字描述符,完全跳过用户态内存。
讲人话就是:A卖房子给C,去中介B那签个合同即可,而不是说A先把房子卖B,B再卖给C
适用场景:文件内容直接通过网络发送(如静态文件服务、大文件传输)。
package main
import (
"io"
"net"
"os"
)
func main() {
// 打开本地大文件
file, _ := os.Open("large_file.dat")
defer file.Close()
// 连接远程服务器
conn, _ := net.Dial("tcp", "remote-server:8080")
defer conn.Close()
// 关键:io.Copy会检测是否支持sendfile,支持则用零拷贝
_, _ = io.Copy(conn, file)
}
标准库io.Copy内部已经实现了零拷贝的系统调用
当io.Copy的源是*os.File,目标是网络连接(如*net.TCPConn)时,Go 会调用sendfile系统调用
数据流程为:磁盘 → 内核文件缓冲区 → 内核套接字缓冲区 → 网卡
全程无用户态内存参与,减少 2 次拷贝(用户态→内核态的来回拷贝)
mmap + write(内存映射)
作用:将文件内容直接映射到用户态内存地址空间,应用程序操作内存即可读写文件,避免read/write的拷贝。
适用场景:需要频繁随机访问文件内容(而非顺序读取),且数据无需修改或修改后直接落盘。
package main
import (
"net"
"syscall"
)
func main() {
// 打开文件(注意需用syscall.Open,获取文件描述符)
fd, _ := syscall.Open("data.txt", syscall.O_RDONLY, 0644)
defer syscall.Close(fd)
// 获取文件大小
stat, _ := syscall.Stat(fd)
size := stat.St_size
// 映射文件到用户态内存(PROT_READ:只读;MAP_SHARED:修改同步到文件)
data, _ := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
defer syscall.Munmap(data) // 必须释放映射
// 这里data是[]byte,你可以读取其内容放入其它变量中,当然也可以直接网络发送
// 发送映射的内存数据(此时data是内核缓冲区的映射,write时减少拷贝)
conn, _ := net.Dial("tcp", "server:8080")
defer conn.Close()
_, _ = conn.Write(data) // 此处write可能优化为直接从映射内存拷贝到套接字缓冲区
}
相比较而言,io.copy在网络传输时零拷贝,使用简单;而syscall.Mmap是真正的零拷贝,不仅可用于网络传输,也可以用于其它的读取业务场景,但使用难度稍微大些
编程语言的设计
零拷贝概念在编程语言的设计中也有体现,并不单一指的就是操作系统层的减少文件内存拷贝,而是一种思想的体现
func parseData(data []byte) {
// 提取头部(不拷贝数据,仅创建新切片)
header := data[:4]
// 提取内容(同样不拷贝)
content := data[4:100]
// 处理header和content...
}
var buf bytes.Buffer
buf.WriteString("hello")
data := buf.Bytes() // 返回内部[]byte的引用,无拷贝
总结
- 静态文件操作
- 读取、不修改
- 节省CPU内存