图1、 用户空间与内核空间

一、传统方式--仅CPU参与的数据拷贝

  • 当应用程序需要读取磁盘数据时,调用 read()从用户态切换到内核态,read()这个系统调用最终由 CPU 来完成;
  • CPU 向磁盘发起 I/O 请求,磁盘收到之后开始准备数据;
  • 磁盘将数据放到磁盘缓冲区之后,向 CPU 发起 I/O 中断,报告 CPU 数据已经 Ready 了;
  • CPU 收到磁盘控制器的 I/O 中断之后,开始拷贝数据,完成之后 read()返回,再从内核态切换到用户态;

拷贝容器日志到宿主机_java

                图2、传统方式数据拷贝

二、直接内存访问(Direct Memory Access,DMA)

1、直接内存访问,指硬件设备绕开 CPU 独立直接访问内存的机制。

2、DMA 在一定程度上解放了 CPU,把之前 CPU 的杂活让硬件直接自己做了,提高了 CPU 效率。

3、DMA需要硬件支持,可以理解为操作系统为提升CPU效率,为网卡、声卡、显卡、磁盘提供的一段特殊驱动程序。

拷贝容器日志到宿主机_java_02

图3、DMA方式数据拷贝

拷贝容器日志到宿主机_系统调用_03

图4、DMA方式数据拷贝的另一种图示 

 DMA与传统方式最主要的变化是:CPU 不再和磁盘直接交互,而是 DMA 和磁盘交互并且将数据从磁盘缓冲区拷贝到内核缓冲区。

传统方式、DMA都存在多次冗余数据拷贝和内核态 &用户态的切换

三、零拷贝(Zero-Copy)

用户空间和内核空间无cpu拷贝,我们称之为0拷贝,实现方式如下:

拷贝容器日志到宿主机_java_04

3.1  mmap 方式(内存映射)

mmap 是 Linux 提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。

注意:映射的文件大小最大只能为1.5G~2G,所以RocketMQ存储消息的文件大小为1G

拷贝容器日志到宿主机_零拷贝_05

 3.2 sendfile 方式

mmap+write 方式有一定改进,但是由系统调用引起的状态切换并没有减少。

sendfile 系统调用是在 Linux 内核 2.1 版本中被引入,它建立了两个文件之间的传输通道。应用程序只需要调用 sendfile 函数即可完成数据拷贝,减少了2次切换。

拷贝容器日志到宿主机_系统调用_06

注意:由于数据不经过用户缓冲区,因此该数据无法被修改。 

3.3 sendfile+DMA 收集

Linux 2.4 内核对 sendfile 系统调用进行优化,但是需要硬件 DMA 控制器的配合。

升级后的 sendfile 将内核空间缓冲区中对应的数据描述信息(如:文件描述符(FD)、地址偏移量等信息)记录到 socket 缓冲区中。

DMA 控制器根据 socket 缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中,从而省去了内核空间中仅剩 1 次 CPU 拷贝。

拷贝容器日志到宿主机_零拷贝_07

可以简单理解为:从内核缓冲区直接拷贝到网卡 

3.4 splice 方式

splice 系统调用是 Linux 在 2.6 版本引入的,splice 系统调用可以在内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。

拷贝容器日志到宿主机_系统调用_08

splice不再需要硬件支持,并且不再限定于 socket 上,即可实现两个普通文件之间的数据零拷贝。

四、各种方式对比

拷贝容器日志到宿主机_数据_09