零拷贝说白了,其实就是传统 IO 的性能实在有点拉胯,所以搞出来一个零拷贝机制提升一下效率。 要了解零拷贝的话,首先得先了解一下传统 IO 的执行流程,这里举个例子,通过传统的 IO 进行网络传输来传输一个文件。
相关参考
文章
尽情阅读,技术进阶,详解mmap原理
一文带你,彻底了解,零拷贝Zero-Copy技术
PS:相关学习文档,后台私信(Linux)
先上一张图,这张图就代表了传统 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 两种,一个一个来说。
mmapmmap 是一种内存映射技术,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 拷贝到协议引擎)。
文章福利 Linux后端开发网络底层原理知识学习提升,后台私信(Linux)获取,完善技术栈,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。
,