零拷贝相信大家都听过了,说白了呢,其实就是传统IO的性能实在有点拉胯,所以搞出来一个零拷贝机制提升一下效率
要了解零拷贝的话,首先得先了解一下传统IO的执行流程,这里举个例子,通过传统的IO进行网络传输来传输一个文件
先上一张图,这张图就代表了传统IO传输文件的流程
- 读取文件的时候,会从用户态切换为内核态,同时基于DMA引擎将磁盘文件拷贝到内核缓冲区
看到这里,可能你就已经懵逼了,什么是用户态和内核态,什么是DMA拷贝,我用大白话解释一下
首先用户态其实就是CPU在执行你的代码,而内核态呢,其实就是你没有那个权限去操作硬件,所以只能交给系统去调用,这个时候就是内核态。举个例子,你的女朋友需要你修个电脑(醒醒,但凡有一粒花生米也不至于喝成这样),我换个说法,假如你同班的女同学想让你修个电脑,但是宿管阿姨不肯放你进女生宿舍,这个时候你就是用户态,你不能进女生宿舍,所以你只能让宿管阿姨(内核态)来帮你把电脑取出来
那什么是DMA拷贝呢,DMA(DirectMemoryAccess,直接内存存取)其实就是因为CPU老哥太累了,所以找了个小弟,就是DMA替他完成一部分的拷贝工作,这样CPU就能去做其他事情了
讲完了内核态和用户态还有DMA的大概意思,我们接着回到刚才的IO流程中,第一步我们将文件从磁盘文件读到了用户缓冲区,此时经历了一次上下文切换和一次拷贝
-
由内核态切换为用户态,基于CPU把内核缓冲区的数据拷贝到用户缓冲区
-
调用socket的输出流的write方法的话,此时会从用户态切换到内核态,同时基于CPU把用户缓冲区里的数据拷贝到Socket缓冲区里去,接着会有一个异步化的过程,基于DMA引擎从Socket缓冲区里把数据拷贝到网络协议引擎里发送出去
-
当IO操作完成之后,又从内核态切换为用户态
通过上面的步骤可以发现传统的IO操作执行,有4次上下文的切换和4次拷贝,是不是很繁琐
零拷贝的话,一般有mmap和sendFile两种,一个一个来说
mmap
mmap是一种内存映射技术,mmap相比于传统的IO来说,其实就是少了1次CPU拷贝而已,上图
传统IO里面从内核缓冲区到用户缓冲区有一次CPU拷贝,从用户缓冲区到Socket缓冲区又有一次CPU拷贝,mmap则一步到位,直接基于CPU将内核缓冲区的数据拷贝到了Socket缓冲区
之所以能够减少一次拷贝,就是因为mmap直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了
虽然减少了一次拷贝,但是上下文切换的次数还是没变
RocketMQ中就是使用的mmap来提升磁盘文件的读写性能
sendFile
在Linux中,提供sendFile函数,实现了零拷贝,依旧是先上图
可以看到在图中,已经没有了用户缓冲区,因为用户缓冲区是在用户空间的,所以没有了用户缓冲区也就意味着不需要上下文切换了,就省略了这一步的从内核态切换为用户态
同时也不需要基于CPU将内核缓冲区的数据拷贝到Socket缓冲区了,只需要从内核缓冲区拷贝一些offset和length到Socket缓冲区
接着从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去;同时从Socket缓冲区里拷贝一些offset和length到网络协议引擎里去,但是这个offset和length的量很少,几乎可以忽略
sendFile整个过程只有两次上下文切换和两次DMA拷贝,很重要的一点是这里完全不需要CPU来进行拷贝了,所以才叫做零拷贝,这里的拷贝指的就是操作系统的层面
那你肯定会问,那mmap里面有一次CPU拷贝为啥也算零拷贝,只能说那不算是严格意义上的零拷贝,但是他确实是优化了普通IO的执行流程,就像老婆饼里也没有老婆嘛
Kafka和Tomcat内部使用就是sendFile这种零拷贝
总结一下:
传统IO执行的话需要4次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和4次拷贝 (磁盘文件DMA拷贝到内核缓冲区,内核缓冲区CPU拷贝到用户缓冲区,用户缓冲区CPU拷贝到Socket缓冲区,Socket缓冲区DMA拷贝到协议引擎)
mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上,适合小数据量读写,需要4次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和3次拷贝(磁盘文件DMA拷贝到内核缓冲区,内核缓冲区CPU拷贝到Socket缓冲区,Socket缓冲区DMA拷贝到协议引擎)
sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送,适合大文件传输,只需要2次上下文切换(用户态 -> 内核态 -> 用户态)和2次拷贝(磁盘文件DMA拷贝到内核缓冲区,内核缓冲区DMA拷贝到协议引擎)