零拷贝(英语: Zero-copy) 技术是指计算机执行操作时, CPU不需要先将数据从某处内存复制到另一个特定区域. 这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽.
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数, 消除传输数据在存储器之间不必要的中间拷贝次数, 从而有效地提高数据传输效率
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销
可以看出没有说不需要拷贝, 只是说减少冗余不必要的拷贝。
技巧💡
将磁盘上的文件读取出来,然后通过网络协议发送给客户端。会有个内核态到用户控件态转换的过程。
- DMA 省不了的步骤,即磁盘文件到内核态的缓存区。DMA是减少了CPU在磁盘文件到内核态缓存区的阻塞,即cpu这时候不是阻塞的。通过中断程序触发cpu处理
IO工作方式演进
传统IO,read + write
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read()
,一次是 write()
。
-
第一次拷贝
,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。 -
第二次拷贝
,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。 -
第三次拷贝
,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。 -
第四次拷贝
,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。技巧💡
要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数.即减少缓存区
mmap + write(减少用户态的拷贝,系统切换没减少)
mmap是Linux提供的一种内存映射文件的机制, 它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射, 从而实现内核缓冲区与用户缓冲区的共享.
这样就减少了一次用户态和内核态的CPU拷贝, 但是在内核空间内仍然有一次CPU拷贝
sendfile方式(减少系统切换,只有一个sendfile)
mmap+write方式有一定改进, 但是由系统调用引起的状态切换并没有减少.
sendfile系统调用是在 Linux 内核2.1版本中被引入, 它建立了两个文件之间的传输通道.
sendfile方式只使用一个函数就可以完成之前的read+write 和 mmap+write的功能, 这样就少了2次状态切换, 由于数据不经过用户缓冲区, 因此该数据无法被修改.
从图中可以看到, 应用程序只需要调用sendfile函数即可完成, 只有2次状态切换, 1次CPU拷贝, 2次DMA拷贝.
但是sendfile在内核缓冲区和socket缓冲区仍然存在一次CPU拷贝, 或许这个还可以优化
sendfile+S-GDMA收集()
Linux 2.4 内核对 sendfile 系统调用进行优化, 但是需要硬件SG-DMA控制器的配合.
升级后的sendfile将内核空间缓冲区中对应的数据描述信息(文件描述符, 地址偏移量等信息)记录到socket缓冲区中.
DMA控制器根据socket缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中, 从而省去了内核空间中仅剩1次CPU拷贝。
比较
无论是传统IO方式, 还是引入零拷贝之后, 2次DMA copy 是都少不了的. 因为两次DMA都是依赖硬件完成的.
应用
netty中的 零拷贝
Netty中的Zero-copy与上面我们所提到到OS层面上的Zero-copy不太一样, Netty的Zero-copy完全是在用户态(Java层面)的, 它的Zero-copy的更多的是偏向于优化数据操作这样的概念.
Netty的Zero-copy体现在如下几个个方面:
-
优化数据操作。减少数据在用户空间的多次拷贝。使用引用
- Netty提供了CompositeByteBuf类, 它可以将多个ByteBuf合并为一个逻辑上的ByteBuf, 避免了各个ByteBuf之间的拷贝.
- 通过wrap操作, 我们可以将byte[]数组, ByteBuf, ByteBuffer 等包装成一个 Netty ByteBuf对象, 进而避免了拷贝操作.
- ByteBuf支持slice 操作, 因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免了内存的拷贝.
-
通过FileRegion包装的FileChannel.tranferTo实现文件传输, 可以直接将文件缓冲区的数据发送到目标Channel, 避免了传统通过循环write方式导致的内存拷贝问题.
-
使用堆外内存,避免数据从JVM Heap到C Heap的拷贝。DirectByteBuffer
在JVM层面, 每当程序需要执行一个I/O操作时, 都需要将数据先从JVM管理的堆内存复制到使用C malloc()或类似函数分配的Heap内存中才能够触发系统调用完成操作, 这部分内存站在Java程序的视角来看就是堆外内存, 但是以操作系统的视角来看其实都属于进程的堆区, OS并不知道JVM的存在, 都是普通的用户程序. 发现了没有, 这样一来JVM在I/O时永远比使用native语言编写的程序多一次数据复制, 这是所有基于VM的编程语言都绕不开的问题, 而且是纯粹的人为多增加了一个步骤. 那么问题来了, 为什么不直接使用JVM堆区数据的地址而是要复制一下呢? 原因很简单, 虚拟机只是一个用户程序, 它本身并没有直接访问硬件的能力, 因此所有的I/O操作都需要借助于系统调用来实现. 在Linux系统中, 与I/O相关的read()和write()系统调用, 都需要传入一个指向你在程序中分配的一片内存区域起始地址的指针, 然后操作系统会将数据填入这片区域或者从这片区域中读出数据. 这里如果直接使用JVM堆中对应byte[]类型的地址的话就会有两个无法解决的问题: 一是Java中的对象实际的内存布局跟C是不一样的, 不同的JVM可能有不同的实现, byte[]的首地址可能只是个对象头, 并不是真实的数据; 二是垃圾收集器的存在使得JVM会经常移动对象的位置, 这样同一个对象的真实内存地址随时都有可能发生变化, JVM知道地址变了, 但是操作系统可不知道. 明确上面这些以后我们就不难理解, Netty中对零拷贝思想的第二处实现, 就是在适当的位置直接使用堆外内存从而避免了数据从JVM Heap到C Heap的拷贝。
rocketMq的零拷贝
资料
【linux】彻底搞懂零拷贝(Zero-Copy)技术 - 知乎