文章目录
- 1. 什么是零拷贝?
- 2. 为什么需要零拷贝?
- 2.1 传统 I/O 的拷贝流程
- 2.2 零拷贝的优化
- 2.2.1 通过 sendfile 系统调用
- 2.2.2 通过 mmap (内存映射) 系统调用
- 3. Netty 实现零拷贝的方式
- 3.1 文件传输优化:FileRegion 封装
- 3.2 直接内存 (Direct Memory)
- 3.3 内存映射文件:MappedByteBuffer
- 3.4 复合缓冲区:CompositeByteBuf
- 3.5 切片与包装操作
- 4. 零拷贝的优点
- 5. 总结
1. 什么是零拷贝?
零拷贝 (Zero-Copy) 是一种 通过减少或避免数据在内存之间不必要的拷贝,从而提升系统性能的技术。它主要用于 操作系统内核 与 用户程序 之间的数据传输场景。
2. 为什么需要零拷贝?
2.1 传统 I/O 的拷贝流程
以文件传输为例,传统 I/O 的拷贝流程如下所示:
- 从 磁盘 读取数据到 内核缓冲区:数据从磁盘通过 DMA(直接内存访问)传输到内核空间的缓冲区。
- 将数据从 内核缓冲区 拷贝到 用户空间:用户程序通过系统调用(如
read()
)将内核缓冲区的数据拷贝到用户空间的缓冲区。
- 将数据从 用户空间 拷贝回 内核 Socket 缓冲区:用户程序通过系统调用(如
write()
)将数据从用户空间缓冲区拷贝到内核空间的 Socket 缓冲区。
- 将数据从 内核 Socket 缓冲区 发送到 网络:数据通过 DMA 传输到网卡,完成发送。
问题:传统流程需要 4 次拷贝 和 2 次上下文切换(用户态 和 内核态 的相互切换),性能开销大。
2.2 零拷贝的优化
零拷贝主要通过 减少用户空间与内核空间的交互 的方式减少拷贝次数,具体可以使用的特性如下:
2.2.1 通过 sendfile 系统调用
通过 sendfile
,直接在内核空间将文件数据传输到 Socket 缓冲区,避免了 2 次上下文切换和 1 次拷贝。步骤如下:
注:第二步文件从内核的 文件缓冲区 拷贝到 Socket 缓冲区 使用的是 DMA 还是 CPU 拷贝需要看硬件是否支持 SG-DMA(分散聚合直接内存访问)。
- 若硬件(如网卡)支持 SG-DMA,数据从 文件缓冲区 到 Socket 缓冲区 的传输由 DMA 完成,无需 CPU 参与拷贝。此时,内核只需告知 DMA 控制器数据位置和长度,DMA 直接在内核空间内搬运数据,完全释放 CPU 资源,实现更高效的零拷贝。
- 若硬件不支持 SG-DMA,内核会通过 CPU 拷贝将数据从 文件缓冲区 复制到 Socket 缓冲区。
2.2.2 通过 mmap (内存映射) 系统调用
mmap
系统调用会将文件映射到进程的虚拟地址空间,使得进程可以直接访问文件内容,而无需将文件内容拷贝到用户空间的缓冲区。过程如下:
- 映射文件到虚拟地址空间:应用程序调用
mmap
,操作系统会创建一个虚拟内存区域,并将其与文件的物理地址进行映射。此时,文件内容并没有被实际加载到内存中,只是建立了映射关系。
- 直接访问映射区域:应用程序可以像访问普通内存一样直接访问映射区域。当应用程序访问映射区域中的某个地址时,如果该地址对应的页面尚未加载到内存中,会触发 缺页中断。操作系统会根据映射关系,将文件中的相应数据从 磁盘 读取到 内核空间的文件缓冲区,并将该页面映射到进程的虚拟地址空间。
- 将数据从 文件缓冲区 发送到到 Socket 缓冲区:当需要将文件内容发送到网络时,应用程序可以直接操作映射区域。操作系统会将内核空间的 文件缓冲区 中的数据直接拷贝到 Socket 缓冲区。
- 将数据从 Socket 缓冲区 发送到 网络:将数据从 Socket 缓冲区 发送到 网卡。
在这个过程中,数据不需要经过用户空间的缓冲区,避免了 1 次数据拷贝。
注意:看似 用户空间 中好像有也一块内存,与 文件缓冲区 一样,但其实 用户空间 用 页表 来映射 文件缓冲区 中内存。
3. Netty 实现零拷贝的方式
3.1 文件传输优化:FileRegion 封装
Netty 通过 FileRegion
类封装了 NIO 的 FileChannel
类的 transferTo()
方法,避免数据 在 内核空间 与 用户空间 的拷贝 和 上下文切换。在 Linux 操作系统中,transferTo()
底层使用 sendfile
的系统调用实现 零拷贝。
3.2 直接内存 (Direct Memory)
Netty 使用 直接内存 (Direct Memory) 来减少数据在 用户空间 和 内核空间 之间的拷贝。直接内存由操作系统直接管理,避免了在 用户空间 的中转。
- 对于文件操作,使用
mmap
进行优化。 - 对于网络传输,使用
sendfile
进行优化。
从而避免将内核空间中 文件缓冲区 的数据拷贝到 用户空间 和 2 次上下文切换。
3.3 内存映射文件:MappedByteBuffer
通过 FileChannel.map()
将文件映射到虚拟内存,直接通过内存地址操作文件数据。底层使用了 mmap
技术,避免了 内核空间 与 用户空间 之间的 数据拷贝 和 线程切换。
3.4 复合缓冲区:CompositeByteBuf
CompositeByteBuf
将多个 ByteBuf
组合成一个逻辑上的整体,无需物理合并数据,减少内存复制。
3.5 切片与包装操作
通过 ByteBuf
对象的 slice()
、duplicate()
或 Unpooled.wrappedBuffer()
共享数据,而非复制数据,使用这些方法生成的 ByteBuf
与原始 ByteBuf
共用底层内存数据。
ByteBuf
除了 slice()
、duplicate()
方法之外,还有 retainedSlice()
、retainedDuplicate()
方法,这两个方法也可以实现零拷贝,而且还 增加了引用计数,避免在原始 ByteBuf
释放时意外释放共享的内存,所以更推荐调用这两个方法。
4. 零拷贝的优点
- 降低 CPU 利用率,减少内存带宽消耗。
- 提升 I/O 密集型任务的性能。
5. 总结
零拷贝并非一次拷贝都没有,它指的是尽量减少拷贝的数量,从而提升程序的性能。可优化的地方在于避免 数据在 内核空间 和 用户空间 的拷贝 和 内核态 与 用户态 之间的上下文切换,在操作系统层面使用 sendfile
或 mmap
就可以做到。在 Netty 中,实现零拷贝的方式有很多种,可以通过以下方式:
FileRegion
对象的transfterTo()
方法- 直接内存
MappedByteBuffer
CompositeByteBuf
- 切片和包装