零拷贝

什么是零拷贝?

计算机操作时,CPU不需要先将数据从某处内存复制到另一个特定的区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽

  1. 零拷贝技术可以减少数据拷贝和共享总线操作的次数,从而提高数据传输的效率;
  2. 零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销。

并不是不需要拷贝,而是减少不必要的拷贝。

应用:Kafka、Netty、RocketMQ等。

Linux的I/O机制与DMA

操作系统内存空间分为用户态和内核态,用户的应用程序无法直接操作硬件,需要通过内核空间进行操作转换,才能真正操作硬件。因此,应用程序需要与网卡、磁盘等硬件进行数据交互时,就需要在用户态和内核态之间来回复制数据。早期,这些操作由CPU负责,压力很大。

DMA(直接内存存取)控制器,接管了数据读写请求,减少CPU的负担。

此时,IO读取,涉及两个过程:

  1. DMA等待数据准备好,把磁盘数据读取到操作系统内核缓冲区;
  2. 用户进程,将内核缓冲区的数据copy到用户空间。

传统数据传送机制

如:读取文件,再用socket发送出去,共2次DMA copy,2次CPU copy,4次上下文切换。

  1. 用户进程调用read(),上下文从用户态转向内核态;
  2. 将磁盘文件,DMA copy到操作系统内核缓冲区;
  3. 将内核缓冲区的数据,CPU copy到应用程序的buffer,上下文从内核态转向用户态;
  4. 用户进程调用write(),上下文从用户态转向内核态;
  5. 将应用程序buffer中的数据,CPU copy到socket网络发送缓冲区(也属于操作系统内核缓冲区);
  6. 将socket buffer中的数据,DMA copy到网卡,进行网络传输,上下文从内核态切换回用户态。

read()、write()换属于系统调用,每次调用涉及2次上下文切换。

image-20230331153118197

Linux支持的零拷贝

mmap内存映射

直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于 DMA 拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。使用mmap代替read(),减少一次CPU拷贝,共2次DMA copy,1次CPU copy,4次上下文切换;

  1. 用户进程调用mmap(),用户态转向内核态;
  2. 磁盘中的数据与应用程序buffer映射,基于DMA copy,内核态转向用户态;
  3. 用户进程调用write(),上下文从用户态转向内核态;
  4. 将应用程序buffer中的数据,CPU copy到socket网络发送缓冲区;
  5. 将socket buffer中的数据,DMA copy到网卡,进行网络传输,上下文从内核态转向用户态。

image-20230331154214104

sendfile

Linux从2.1支持sendfile

调用sendfile()时,DMA将磁盘数据复制到操作系统内核缓冲区,然后直接将内核buffer中的数据长度和描述符直接拷贝到socket buffer。如果设备支持,无需CPU拷贝。共1(或0)次CPU拷贝,2次DMA拷贝,2次上下文切换。

  1. 用户进程调用sendfile(),上下文从用户态转向内核态;
  2. 将磁盘文件中的数据,DMA copy到操作系统内核缓冲区;
  3. 将内核buffer中数据的长度和描述符,CPUcopy到socket buffer;(设备支持的话,此次CPU copy可以省略)
  4. 将socket buffer中的数据,DMA copy到网卡,进行网络传输,从内核态转向用户态。

image-20230331155825079

splice

Linux从2.6.17支持splice

数据从磁盘读取到操作系统内核缓冲区后,直接将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。

从磁盘读取到内核 buffer 后,在内核空间直接与 socket buffer 建立 pipe管道

与sendfile()的区别:

  1. send file 0 CPU copy需要硬件支持,splice()不需要

image-20230331160122231

Java支持的零拷贝

Java支持内存映射mmap、sendfile。

NIO提供的内存映射MappedByteBuffer

NIO中的FileChanel.map()采用了内存映射方式,底层就是调用Linux mmap()实现的。

适合读取大文件,同时也能对文件内容进行更改。

NIO 提供的 sendfile

Java NIO 中提供的 FileChannel 拥有 transferTo 和 transferFrom 两个方法,可直接把FileChannel 中的数据拷贝到另外一个 Channel,或者直接把另外一个 Channel 中的数据拷贝到 FileChannel。该接口常被用于高效的网络 /文件的数据传输和大文件拷贝。

Kafka中的零拷贝

  1. Producer生产的数据持久化到broker,broker采用mmap文件映射,实现顺序的快速写入;
  2. Consumer从broker读取数据,broker采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。

Netty中的零拷贝

  1. 网络通信上,Netty接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果采用传统的堆内存进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中,相对于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝;

  2. 缓存操作上,Netty提供了CompositeByteBuf类,可以将多个ByteBuffer合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝;

    通过 wrap 操作,我们可以将 byte[]数组、ByteBuf、 ByteBuffer 等包装成一个 NettyByteBuf 对象,进而避免了拷贝操作。
    ByteBuf支持slice 操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。

  3. 文件传输上,Netty通过FileRegion包装的FileChannel.tranferTo实现文件传输,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

请作者喝瓶肥宅快乐水