内存映射
(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
RocketMQ为什么快?kafka为什么快?什么是mmap?这些问题都逃不过一个点,就是零拷贝。
虽然还有其他的原因,但是这里主要讨论零拷贝。
传统的IO方式
传统的IO方式底层其实是调用read和write来实现;
- 用户进程通过read向操作系统发起系统调用,指示上下文从用户态转向内核态;
- DMA控制器把数据从硬盘读取到内核缓冲区;
- CPU把内核读缓冲区的数据拷贝到应用缓冲区,上下文从内核态转为用户态,read返回;
- 用户进程通过write方法发起调用,上下文从用户态切换为内核态;
- CPU将用户/应用缓冲区中的数据拷贝到socket缓冲区(写缓冲);
- DMA控制器把数据从socket缓冲区拷贝到网卡(写入网卡设备),上下文从内核态切换回用户态,write返回。
什么是DMA拷贝?
对于一个IO操作而言,都是通过CPU发出对应的指令来完成的,但是相比于CPU来说,IO的速度太慢了,CPU有大量时间处于等待IO的状态,因此就产生了DMA直接内存访问技术,本质上来说DMA就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少了CPU的等待时间,但是不论是谁来拷贝,频繁的拷贝耗时也是对性能的影响。
一次简单的传统IO过程,发生了4次用户态和内核态的上下文切换,这在高并发场景下无疑会对性能产生极大的影响。
这整个过程中发生了4次用户态和内核态的上下文切换和4次的拷贝,如下图:
什么是零拷贝?
零拷贝技术是指计算机执行操作的时候,CPU不需要先将数据从某处内存复制到另一个特定的区域,这种技术通常用于网络传输文件时节省CPU周期和内存带宽。
那么针对零拷贝而言,并非是真的没有数据拷贝的过程,只不过是减少了用户态和内核态的切换次数,以及CPU的拷贝次数。
下面来谈谈几种常见的零拷贝技术:
1. mmap + write
简单来说就是通过mmap替换了read + write中的read操作,减少了一次CPU的拷贝。
mmap的主要实现方式是将内核读缓冲区中的地址和用户缓冲区中的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝,那整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,流程如下:
- 用户通过mmap方法向操作系统发起调用,上下文从用户态转向内核态;
- DMA控制器把数据从硬盘中拷贝到都缓冲区;
- 上下文从内核态转为用户态,mmap调用返回;
- 用户进程通过write方法发起调用,上下文从用户态切换为内核态;
- CPU将内核读缓冲区中的数据拷贝到socket缓冲区(写缓冲);
- DMA控制器把数据从socket缓冲区拷贝到网卡(写入网卡设备),上下文从内核态切换回用户态,write返回。
总的来说,mmap + write的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
2. sendfile()方式
相比于mmap + write的方式来说,sendfile同样减少了一次CPU拷贝,而且还减少了两次上下文切换。
sendfile()是Linux2.1内核版本之后引入的一个系统调用函数,通过使用sendfile,数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于sendfile()替代read+write从而减少了一次系统调用(两次用户态和内核态的上下文切换),具体流程如下:
总结
由于CPU和IO速度的差异问题产生DMA技术,通过DMA搬运来减少CPU的等待时间。
- 传统IO:2次DMA拷贝+2次CPU拷贝+4次上下文切换
- mmap:2次DMA拷贝+1次CPU拷贝+4次上下文切换
- sendfile():2次DMA拷贝+1次CPU拷贝+2次上下文切换
但是使用sendfile()方式时,IO数据对于用户空间是不可见的。