传统IO
- 应用调用read方法向操作系统发起读数据的请求,此时由用户态切换为内核态
- 当系统收到读数据请求时,利用DMA控制器把数据从磁盘读取到系统缓存区中(图中2.1)
- 再然后CPU会把系统缓存区的数据写应用缓存区(图2.2),此时由内核态切换为用户态
- 应用再调用write方法通知系统进行数据的写操作,此时由用户态切换为内核态。
- CPU把应用缓存中的数据写到系统缓存区(图2.3)
- 再然后就是DMA控制器再把数据从系统缓存区写到网卡缓存区上(图2.4),write方法返回,此时由内核态切换为用户态。
在普通的IO拷贝时要有4次的上下文切换过程和4次的拷贝过程,在高并发的场景下对性能会产生比较大的影响。
DMA
数据在读写的过程中都需要CPU发出对应的命令来完成,因为CPU的速度比IO操作要快的多,在数据拷贝的过程中CPU不可能一直处于等待的过程,但是如果不等,CPU又不知道IO什么时候处理完,为了协调高速的CPU与低速的IO的矛盾,因此引入了DMA,DMA(Direct Memory Access)直接内存访问技术,通过它来进行内存和IO设备的数据传输。大至就是CPU给DMA发一个指令,然后DMA开始干活,然后CPU干别的活,DMA干完后告诉CPU,从而减少了CPU的等待时间。
零值拷贝
观察图可以2.1 和2.2 感觉数据在此过程做了个无用功,从内核态搬到用户态,再由用户态搬到内核态,因此提出了零值拷贝,零值拷贝不是没有拷贝,而是不再有用户态和内核态间数据拷贝过程,他的实现有mmap和sendfile两种方式。
mmap
mmap (member map)内存映射,数据读取到系统缓存区后,用户态数据在系统缓存区的映射就可以完成数据的传输过程。
- 首先向操作系统发送一个mmap命令(上图1.1),此时由用户态切换为内核态。
- 系统收到mmap命令后,会用DMA把数据从磁盘读到到内存的缓存区中(上图1.2)。
- 返回数据缓存区的一个映射mmap(上图1.3),由内核态切换到用户态。
- 用户在发起一个write的操作(上图1.4),由用户态切换到内核态。
- cpu再把内存缓存区中的数据拷贝到另一个缓存区上(上图1.5)
- DMA把数据拷贝到网卡上后返回(上图1.6),并由内核态切换到用户态
mmap的操作共有4个上下文的切换和3次的数据拷贝过程,比普通的拷贝少了一次cpu的拷贝。
sendfile
mmap的拷贝方法要先获取数据区的映射,然后有用户发起写数据的命令后,在往网卡上写,如果不需要对数据做任何处理,只是原封不动的把数据发送出去,mmap命令和write命令可以合并为一个命令,直接告诉操作系统往外发送哪个数据即可,合并后的命令就sendfile。
- 用户往操作系统发直sendfile的命令,此时有用户态切换到内核态
- 系统收到命令后,由DMA把数据从磁盘上读取到系统缓存区上
- 然后由cpu把读到的到数据拷贝到另一个缓存区上
- 再由DMA把数据写到网上上
- 然后返回,由内核态切换到用户态
sendfile的过程由2次上下文的切换和3次的数据拷贝过程,整个过程读到的数据对用户空间不可见,适应用静态文件服务器。
sendfile升级
在sendfile的cpu把数据从缓存区从一个地方拷贝到另一个地方,也会浪费额外的空间,能不能在往网卡上写数据时只用第一次DMA拷贝出来的数据,直接拷贝到网卡上呢?因此对sendfile再次升级,引入了DMA Scatter/Gather 分散/收集功能。
- 用户发起sendfile命令(上图1.1),系统收到后,由用户态 切换到内核态
- 调用DMA利用scatter从磁盘读取数据到缓存区离散存储(上图1.2)
- cpu把缓存区数据的文件描述符和数据长度发送给另一个缓存区(上图1.3)
- DMA在从另一个缓存区读取数据时根据文件描述符和数据长度,使用scatter/gather从缓存区中读取到网卡上(上图1.4)
- 返回,并有内核态切换到用户态。
此过程对用户空间仍然是不可见的,而且需要硬件的支持。有2次上下文切换和2次的拷贝过程,性能在大幅的提高。
场景
kafka和rocketmq都用到了mmap技术,两个中间件都有消息的持久化过程,和消息的读取过程。
mq对消息持久化和读取的过程都用到了mmap+write,而kafka则是持久化的过程用到了mmap+write,读取消息用的sendfile。
总结
为什么需要DMA?
DMA是为了解决高速CPU和低速IO操作的矛盾,cpu发起一个读取指令后,由dma接管数据的读取复制工作,cpu处理别的事件。