起初,零拷贝指:CPU不参与数据拷贝,而非没有拷贝动作。目前只要减少不必要的数据拷贝,都称为零拷贝。

零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU 不需要先将数据从某处内存 复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽。
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率 。
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间,因为上下文切换而带来的开销。
零拷贝不是不需要拷贝,只是减少冗余[不必要]的拷贝。
Kafka、Netty、Rocketmq、Nginx、Apache均使用了零拷贝技术

Java中实现零拷贝组件:NIO、kafka、Netty。
相关技术MMAP、sendFile

DMA技术

在早期计算机中,用户进程需要读取磁盘数据,需要 CPU 中断和 CPU 参与,因此效率比较低,发起 IO 请求,每次的 IO 中断,都带来 CPU 的上下文切换。
DMA(Direct Memory Access,直接内存存取) 技术,允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。
DMA 控制器,接管了数据读写请求,减少 CPU参与简单的文件拷贝,提高了CPU效率。 现代硬盘基本都支持 DMA。
IO 读取两个过程
1、DMA 等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
2、用户进程,将内核缓冲区的数据复制到用户空间。

传统网络数据传送过程

比如:读取文件,再用 socket 发送出去,实际经过四次 copy。
伪码实现如下:
buffer = File.read()
Socket.send(buffer)
4次拷贝(2次DMA拷贝,2次CPU拷贝)、4次上下文切换,

拷贝分析

image.png
1、第一次:将磁盘文件,读取到操作系统内核缓冲区
2、第二次:将内核缓冲区的数据,copy到应用程序的buffer
3、第三步:将应用程序buffer 中的数据,copy 到 socket 网络发送缓冲区(属 于操作系统内核的缓冲区);
4、第四次:将socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
虽然引入DMA 来避免CPU在拷贝中的中断,但四次 copy 仍存在“不必要拷贝”的。实际上并不需要第二个和第三个数据副本,第二次和第三次数据 copy带来额外开销。因为应用程序并没有修改数据。所以,数据可以直接从读缓冲区传输到套接字缓冲区
这也正是零拷贝出现的背景和意义。

上下文切换分析

read 和 send 都属于系统调用,每次调用都牵涉到两次上下文切换
image.png

Linux零拷贝技术

目的:减少 IO 流程中不必要的拷贝,当然零拷贝需要 OS 支持,也就是需要 kernel 暴露 api。

mmap 内存映射

image.png
硬盘上文件的位置应用程序缓冲区(application buffers)进行映射(建立一种一一对应 关系),由于 mmap()将文件直接映射到用户空间,所以实际文件读取时根据这个映射关系, 直接将文件从硬盘拷贝到用户空间,只进行了一次数据拷贝,减少文件内容从硬盘拷贝到内核空间的缓冲区
mmap 内存映射将会经历:3 次拷贝(1 次 cpu拷贝,2 次 DMA copy);4 次上下文切换,调用 mmap 函数 2 次,write 函数 2 次。

sendfile

image.png
linux 2.1 支持的 sendfile
当调用 sendfile()时,DMA 将磁盘数据复制到 kernel buffer,然后将内核中的 kernel buffer 直接拷贝到socket buffer;但是数据并未被真正复制到 socket 关联的缓冲区内。取而代之的是,只有记录数据位置和长度的描述符被加入到 socket 缓冲区中。
DMA 模块将数据直接从内核缓冲区传递给协议引擎,从而消除了遗留的最后一次复制。但是要注意,这个需要 DMA 硬件设备支持,如果不支持,CPU 就必须介入进行拷贝。
一旦数据全都拷贝到 socket buffer,sendfile()系统调用将会 return、代表数据转化的完成。socket buffer 里的数据就能在网络传输了。
sendfile 会经历
3(如果硬件设备支持,则2)次拷贝。
1(如果硬件设备支持,则0)次 CPU copy, 2 次 DMA copy;
2 次上下文切换

splice

image.png
Linux 从 2.6.17 支持 splice
数据从磁盘读取到 OS 内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间
如图所示,从磁盘读取到内核 buffer 后,在内核空间直接与 socket buffer 建立 pipe 管道。
和 sendfile()不同的是,splice()不需要硬件支持。
注意 splice 和 sendfile 的不同,sendfile 是 DMA 硬件设备不支持的情况下将磁盘数据加载到 kernel buffer 后,需要一次 CPU copy,拷贝到 socket buffer。而 splice 是更进一步,直接将两个内核空间的 buffer 进行 pipe
splice 会经历 2 次拷贝: 0 次 cpu copy 2 次 DMA copy; 2 次上下文切换。

总结 Linux 中零拷贝

最早的零拷贝定义,来源于Linux 2.4 内核新增 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核
态 Buffer 后,直接通过 DMA 拷贝到 NIO Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。这是真正操作系统意义上的零拷贝(也就是狭义零拷贝)。
随着发展,零拷贝的概念得到了延伸,就是目前的减少不必要的数据拷贝都算作零拷贝的范畴。

Java 生态圈中的零拷贝

Linux 提供的零拷贝技术 Java 仅支持 2 种(内存映射 mmap、sendfile);

NIO 提供的内存映射 MappedByteBuffer

NIO中的FileChannel.map()方法,就是采用了操作系统中的内存映射方式,底层调用Linux mmap()实现的。 将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。
这种方式适合读取大文件, 同时也能对文件内容进行更改,但是如果其后要通过 SocketChannel发送,还是需要 CPU 进行数据的拷贝。

NIO 提供的 sendfile

Java NIO 中提供的FileChannel 拥有 transferTo 和 transferFrom 两个方法,可直接把FileChannel 中的数据拷贝到另外一个 Channel,或者直接把另外一个 Channel 中的数据拷贝到 FileChannel。该接口常被用于高效的网络 / 文件的数据传输和大文件拷贝。
在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于 Java IO 中提供的方法。

Kafka 中的零拷贝

Kafka 两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝。
一是Producer 生产的数据存到 broker
Producer 生产的数据持久化到 broker,broker 里采用mmap文件映射,实现顺序的快速 写入。
二是 Consumer 从 broker 读取数据。
Customer从broker读取数据,broker里采用sendfile,将磁盘文件读到OS 内核缓冲区后,直接转到 socket buffer进行网络发送。

Netty 的零拷贝实现

Netty 的零拷贝主要包含三个方面:

  1. - 在网络通信上,Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用**堆外直接内存**进行 Socket 读写,**不需要**进行字节缓冲区的**二次拷贝**。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
  2. - 在缓存操作上,Netty提供了CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,**避免了各个 ByteBuf 之间的拷贝**。 通过 wrap 操作,我们可以将 byte[]数组、ByteBuf ByteBuffer 等包装成一个 Netty ByteBuf 对象,进而避免了拷贝操作。ByteBuf支持slice 操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
  3. - 在文件传输上,Netty 的通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,它可以直接将文件缓冲区的数据发送到目标 Channel,**避免**了传统通过循环 write 方式导致的**内存拷贝**问题。