零拷贝详解

一、什么是零拷贝

零拷贝并不是指在数据的传输过程中发生拷贝的次数为零,而是指数据在传输过程中从内核空间到用户空间之间的数据拷贝次数为零,数据可以直接从内核缓冲区拷贝到应用程序中,避免了数据的多次拷贝,从而提高了数据传输的效率。

二、用户空间与内核空间

用户空间和内核空间是操作系统中的两个不同的地址空间,它们分别用于执行不同的代码和管理不同的资源。在操作系统中,用户空间和内核空间之间的隔离是非常重要的,它可以保护操作系统和应用程序的安全,防止恶意代码对系统造成损害。

  • 用户空间
    是指应用程序和用户可以直接访问的内存区域,它是一个受限的地址空间。在用户空间中,应用程序可以直接使用系统提供的API接口,通过系统调用等方式与内核进行通信,请求系统资源和服务,如打开文件、创建进程、读写网络等。应用程序在用户空间中运行时,具有较小的特权级别,无法直接访问操作系统的核心资源,如硬件设备、系统内核等。
  • 内核空间
    是指操作系统的核心代码和资源所在的内存区域,它是一个高度保护的地址空间。在内核空间中,操作系统具有最高的特权级别,可以直接访问硬件资源和系统资源,如CPU、内存、外设等。内核空间中运行的代码可以执行一些敏感操作,如修改硬件状态、控制系统资源等。

三、传统IO流程

  • DMA copy:DMA 的全称叫直接内存存取(Direct Memory Access),是一种允许外围设备(硬件子系统)直接访问系统主内存的机制,也就是说通过DMA技术可以直接将硬件设备中的数据读取到内核中。目前大多数的硬件设备,包括磁盘控制器、网卡、显卡以及声卡等都支持 DMA 技术。
  • CPU copy:主要是由CPU负责进行数据之间的拷贝。

以读取硬盘上的txt文件为例,从硬盘读取到拷贝到网卡发送出去整个流程如下:
零拷贝详解_第1张图片

  1. 首先由用户空间发出 read() 系统调用,此时由用户空间切换到内核空间(第一次上下文切换)。
  2. 通过DMA技术将硬盘上的txt文件数据读取到内核空间缓冲区(第一次拷贝)。
  3. 再通过CPU将内核缓冲区的数据拷贝到用户缓冲区中(第二次拷贝)。
  4. 然后 read() 系统调用返回,此时由内核空间切换到用户空间(第二次上下文切换)。
  5. 当数据拷贝到用户缓冲区后,由用户空间发出 write() 系统调用,这时又从用户空间切换到内核空间(第三次上下文切换)。
  6. 再由CPU将用户缓冲区的数据拷贝到socket缓冲区(第三次拷贝)。
  7. 然后通过DMA技术将socket缓冲区的数据拷贝到网卡上(第四次拷贝)。
  8. 最后数据完成拷贝到网卡后,write() 系统调用返回,由内核空间切换到用户空间(第四次上下文切换)。

总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,2次DMA拷贝,2次CPU拷贝。其实从内核空间到用户空间内存的拷贝是完全不必要的,因为除了将数据转储到不同的buffer之外,我们没有做任何其他的事情。而零拷贝所做的正是省略了内核空间到用户空间内存的拷贝,从而大大优化了数据的传输效率。

四、零拷贝实现的几种方式

1. mmap+write

mmap 是 Linux 提供的一种内存映射的方法,使用 mmap 的目的是将内核缓冲区与用户空间的缓冲区进行映射。从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核缓冲区拷贝到用户缓冲区的过程。
零拷贝详解_第2张图片

  1. 首先由用户空间发出 mmap() 系统调用,由用户空间切换到内核空间(第一次上下文切换)。
  2. 通过DMA技术将硬盘上的txt文件数据读取到内核空间缓冲区(第一次拷贝)。
  3. 然后 mmap() 系统调用返回,此时由内核空间切换到用户空间(第二次上下文切换)。
  4. 由于用户空间的共享区域与内核空间的缓冲区做了内存映射,因此用户空间通过共享区域就能获取到内核缓冲区的数据,也就无需再将内核缓冲区的数据拷贝的用户缓冲区了。
  5. 再由用户空间发出 write() 系统调用,这时又从用户空间切换到内核空间(第三次上下文切换)。
  6. 接着由CPU将内核缓冲区的数据拷贝到socket缓冲区(第二次拷贝)。
  7. 再通过DMA技术将socket缓冲区的数据拷贝到网卡上(第三次拷贝)。
  8. 最后数据完成拷贝到网卡后,write() 系统调用返回,由内核空间切换到用户空间(第四次上下文切换)。

总的来说,mmap方式进行了4次用户空间与内核空间的上下文切换,2次DMA拷贝,1次CPU拷贝。它与传统I/O相比仅仅少了1次内核空间缓冲区和用户空间缓冲区之间的CPU拷贝。mmap 主要的用处是提高 I/O 性能,特别是针对大文件。对于小文件,内存映射文件反而会导致碎片空间的浪费。

2. Sendfile

通过 sendfile() 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。
零拷贝详解_第3张图片

  1. 首先由用户空间发出 sendfile() 系统调用,由用户空间切换到内核空间(第一次上下文切换)。
  2. 通过DMA技术将硬盘上的txt文件数据读取到内核空间缓冲区(第一次拷贝)。
  3. 接着由CPU将内核缓冲区的数据拷贝到socket缓冲区(第二次拷贝)。
  4. 再通过DMA技术将socket缓冲区的数据拷贝到网卡上(第三次拷贝)。
  5. 最后 sendfile() 系统调用返回,由内核空间切换到用户空间(第二次上下文切换)。

通过sendfile只使用了2次用户空间与内核空间的上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝。Sendfile 调用中 I/O 数据对用户空间是完全不可见的,也就是说,这是一次完全意义上的数据传输过程,用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。

3. Sendfile+DMA Gather Copy

这种只适用于将数据从文件拷贝到 socket 套接字上的传输过程。它将内核缓冲区中对应的数据描述信息(内存地址、地址偏移量)记录到相应的socket缓冲区中,由 DMA 根据内存地址、地址偏移量将数据批量地从内核缓冲区拷贝到网卡设备中。这样 DMA 引擎直接利用 gather 操作将页缓存中数据打包发送到网络中即可,本质就是和虚拟内存映射的思路类似。
零拷贝详解_第4张图片

  1. 首先由用户空间发出 sendfile() 系统调用,由用户空间切换到内核空间(第一次上下文切换)。
  2. 通过DMA技术将硬盘上的txt文件数据读取到内核空间缓冲区(第一次拷贝)。
  3. 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。
  4. DMA Gather Copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到网卡设备中(第二次拷贝)。
  5. 最后 sendfile() 系统调用返回,由内核空间切换到用户空间(第二次上下文切换)。

整个拷贝过程会发生 2 次上下文切换、0 次 CPU 拷贝以及 2 次 DMA 拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。

4. Splice

Splice相当于在Sendfile+DMA Gather Copy上的提升,Splice 系统调用可以在内核缓冲区和socket缓冲区之间建立管道从而避免了两者之间的 CPU 拷贝操作。
零拷贝详解_第5张图片

  1. 首先由用户空间发出 splice() 系统调用,由用户空间切换到内核空间(第一次上下文切换)。
  2. 通过DMA技术将硬盘上的txt文件数据读取到内核空间缓冲区(第一次拷贝)。
  3. 再通过内核空间缓冲区与socket缓冲区之间的管道进行数据的流转,从而省去了一次CPU的拷贝。
  4. 然后通过DMA技术将socket缓冲区的数据拷贝到网卡上(第二次拷贝)。
  5. 最后 splice() 系统调用返回,由内核空间切换到用户空间(第二次上下文切换)。

Splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,0 次 CPU 拷贝以及 2 次 DMA 拷贝。

五、零拷贝的实际应用

1. Java NIO基于零拷贝的实现

(1)MappedByteBuffer.map():底层调用了操作系统的mmap()内核函数。
(2)DirectByteBuffer.allocateDirect():可以直接创建基于本地内存的缓冲区。
(3)FileChannel.transferFrom()/transferTo():底层调用了sendfile()内核函数。

2. Netty中零拷贝的应用

(1)Netty的发送、接收数据的ByteBuf缓冲区,默认会使用堆外本地内存创建,采用直接内存进行Socket读写,数据传输时无需经过二次拷贝。
(2)Netty的文件传输采用了transferTo()/transferFrom()方法,它可以直接将文件缓冲区的数据发送到目标Channel(Socket),底层就是调用了sendfile()内核函数,避免了文件数据的CPU拷贝过程。
(3)Netty提供了组合、拆解ByteBuf对象的API,咱们可以基于一个ByteBuf对象,对数据进行拆解,也可以基于多个ByteBuf对象进行数据合并,这个过程中不会出现数据拷贝,这个是程序级别的零拷贝。

3. Kafka中零拷贝的应用

基于java.nio包下的FileChannel.transferTo()实现零拷贝。Kafka Server基于FileChannel将文件中的消息数据发送到SocketChannel。

4. RocketMQ中零拷贝的应用

基于mmap + write的方式实现零拷贝。内部实现基于nio提供的java.nio.MappedByteBuffer,基于FileChannel的map方法得到mmap的缓冲区。

你可能感兴趣的:(netty,网络,运维,netty)