图1、 用户空间与内核空间
一、传统方式--仅CPU参与的数据拷贝
- 当应用程序需要读取磁盘数据时,调用 read()从用户态切换到内核态,read()这个系统调用最终由 CPU 来完成;
- CPU 向磁盘发起 I/O 请求,磁盘收到之后开始准备数据;
- 磁盘将数据放到磁盘缓冲区之后,向 CPU 发起 I/O 中断,报告 CPU 数据已经 Ready 了;
- CPU 收到磁盘控制器的 I/O 中断之后,开始拷贝数据,完成之后 read()返回,再从内核态切换到用户态;
图2、传统方式数据拷贝
二、直接内存访问(Direct Memory Access,DMA)
1、直接内存访问,指硬件设备绕开 CPU 独立直接访问内存的机制。
2、DMA 在一定程度上解放了 CPU,把之前 CPU 的杂活让硬件直接自己做了,提高了 CPU 效率。
3、DMA需要硬件支持,可以理解为操作系统为提升CPU效率,为网卡、声卡、显卡、磁盘提供的一段特殊驱动程序。
图3、DMA方式数据拷贝
图4、DMA方式数据拷贝的另一种图示
DMA与传统方式最主要的变化是:CPU 不再和磁盘直接交互,而是 DMA 和磁盘交互并且将数据从磁盘缓冲区拷贝到内核缓冲区。
传统方式、DMA都存在多次冗余数据拷贝和内核态 &用户态的切换
三、零拷贝(Zero-Copy)
用户空间和内核空间无cpu拷贝,我们称之为0拷贝,实现方式如下:
3.1 mmap 方式(内存映射)
mmap 是 Linux 提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。
注意:映射的文件大小最大只能为1.5G~2G,所以RocketMQ存储消息的文件大小为1G
3.2 sendfile 方式
mmap+write 方式有一定改进,但是由系统调用引起的状态切换并没有减少。
sendfile 系统调用是在 Linux 内核 2.1 版本中被引入,它建立了两个文件之间的传输通道。应用程序只需要调用 sendfile 函数即可完成数据拷贝,减少了2次切换。
注意:由于数据不经过用户缓冲区,因此该数据无法被修改。
3.3 sendfile+DMA 收集
Linux 2.4 内核对 sendfile 系统调用进行优化,但是需要硬件 DMA 控制器的配合。
升级后的 sendfile 将内核空间缓冲区中对应的数据描述信息(如:文件描述符(FD)、地址偏移量等信息)记录到 socket 缓冲区中。
DMA 控制器根据 socket 缓冲区中的地址和偏移量将数据从内核缓冲区拷贝到网卡中,从而省去了内核空间中仅剩 1 次 CPU 拷贝。
可以简单理解为:从内核缓冲区直接拷贝到网卡
3.4 splice 方式
splice 系统调用是 Linux 在 2.6 版本引入的,splice 系统调用可以在内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
splice不再需要硬件支持,并且不再限定于 socket 上,即可实现两个普通文件之间的数据零拷贝。