深刻剖析神秘的“零拷贝”

前言

"零拷贝"这三个字,想必你们多多少少都有听过吧,这个技术在各类开源组件中都使用了,好比kafka,rocketmq,netty,nginx等等开源框架都在其中引用了这项技术。因此今天想和你们分享一下有关于零拷贝的一些知识。java

计算机中数据传输

在介绍零拷贝以前我想说下在计算机系统中数据传输的方式。数据传输系统的发展,为了写这一部分又祭出了我尘封多年的计算机组成原理:nginx

早期阶段:

分散链接,串行工做,程序查询。 在这个阶段,CPU就像个保姆同样,须要手把手的把数据从I/O接口从读出而后再送给主存。 这个阶段具体流程是:面试

  1. CPU主动启动I/O设备
  2. 而后CPU一直问I/O设备老铁你准备好了吗,注意这里是一直询问。
  3. 若是I/O设备告诉了CPU说:我准备好了。CPU就从I/O接口中读数据。
  4. 而后CPU又继续把这个数据传给主存,就像快递员同样。

这种效率很低数据传输过程一直占据着CPU,CPU不能作其余更有意义的事。缓存

接口模块和DMA阶段

这一部分介绍的也是咱们后面具体网络

接口模块

在冯诺依曼结构中,每一个部件之间均有单独连线,不只先多,并且致使扩展I/O设备很不容易,咱们上面的早期阶段就是这个体系,叫做分散链接。扩展一个I/O设备得链接不少线。因此引入了总线链接方式,将多个设备链接在同一组总线上,构成设备之间的公共传输通道。 这个也是如今咱们家用电脑或者一些小型计算器的数据交换结构。app

在这种模式下数据交换采用程序中断的方式,咱们上面知道咱们启动I/O设备以后一直在轮询问I/O设备是否准备好,要是把这个阶段去掉了就行了,程序中断很好的实现了咱们的夙愿:框架

  1. CPU主动启动I/O设备。
  2. CPU启动以后不须要再问I/O,开始作其余事,相似异步化。
  3. I/O准备好了以后,经过总线中断告诉CPU我已经准备好了。
  4. CPU进行读取数据,传输给主存中。

DMA

虽然上面的方式虽然提升了CPU的利用率,可是在中断的时候CPU同样是被占用的,为了进一步解决CPU占用,又引入了DMA方式,在DMA方式中,主存和I/O设备之间有一条数据通路,这下主存和I/O设备之间交换数据时,就不须要再次中断CPU。异步

通常来讲咱们只须要关注DMA和中断两种便可,下面介绍的都是用来适合大型计算机的一些,这里只说简单的过一下:socket

具备通道结构的阶段

在小型计算机中采用DMA方式能够实现高速I/O设备与主机之间组成数据的交换,但在大中型计算机中,I/O配置繁多,数据传送平凡,若采用DMA方式会出现一系列问题。性能

  • 每台I/O设备都配置专用额DMA接口,不只增长了硬件成本,并且解决DMA和CPU访问冲突问题,会使控制变得十分复杂。
  • CPU须要对众多的DMA接口进行管理,一样会影响工做效率。

因此引入了通道,通道用来管理I/O设备以及主存与I/O设备之间交换信息的部件,能够视为一种具备特殊功能的处理器。它是从属于CPU的一个专用处理器,CPU不直接参与管理,故提升了CPU的资源利用率

具备I/O处理机的阶段

输入输出系统发展到第四阶段,出现了I/O处理机。I/O处理机又称为外围处理机,它独立于主机工做,既能够完成I/O通道要完成的I/O控制,又完成格式处理,纠错等操做。具备I/O处理机的输出系统与CPU工做的并行度更高,这说明I.O系统对主机来讲具备更大的独立性。

小结

咱们能够看到数据传输进化的目标是一直在减小CPU占有,提升CPU的资源利用率。

数据拷贝

先介绍一下今天咱们的需求,在磁盘中有个文件,如今须要经过网络传输出去。 若是是你应该怎么作?经过上面的一些介绍,相信你心中应该有些想法了吧。

传统拷贝

若是咱们用Java代码实现的话用咱们会有以下的的实现:伪代码参考以下:

public static void main(String[] args) {
        Socket  socket = null;
        File file = new File("test.file");
        byte[] b = new byte[(int) file.length()];
       
        try {
            InputStream in = new FileInputStream(file);
            readFully(in, b);
            socket.getOutputStream().write(b);
        } catch (Exception e) {
            
        }
    }

    private static boolean readFully(InputStream in, byte[] b) {
        int size = b.length;
        int offset = 0;
        int len;
        for (; size > 0;) {
            try {
                len = in.read(b, offset, size);
                if (len == -1) {
                    return false;
                }
                offset += len;
                size -= len;
            } catch (Exception ex) {
                return false;
            }
        }
        return true;
    }

这是咱们传统的拷贝方式具体的数据流转图以下,PS:这里不考虑Java中传输数据时须要先将堆中的数据拷贝到直接内存中。

能够看见咱们总管须要经历四个阶段,2次DMA,2次CPU中断,总共四次拷贝,有四次上下文切换,而且会占用两次CPU。

  1. CPU发指令给I/O设备的DMA,由DMA将咱们磁盘中的数据传输到内核空间的内核buffer。
  2. 第二阶段触发咱们的CPU中断,CPU开始将将数据从kernel buffer拷贝至咱们的应用缓存
  3. CPU将数据从应用缓存拷贝到内核中的socket buffer.
  4. DMA将数据从socket buffer中的数据拷贝到网卡缓存。

优势:开发成本低,适合一些对性能要求不高的,好比一些什么管理系统这种我以为就应该够了

缺点:屡次上下文切换,占用屡次CPU,性能比较低。

sendFile实现零拷贝

上面是零拷贝呢?在wiki中的定位:一般是指计算机在网络上发送文件时,不须要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

在java NIO中FileChannal.transferTo()实现了操做系统的sendFile,咱们能够同下面伪代码完成上面需求:

public static void main(String[] args) {
        SocketChannel socketChannel = SocketChannel.open();
        FileChannel fileChannel = new FileInputStream("test").getChannel();
        fileChannel.transferTo(0,fileChannel.size(),socketChannel);

    }

咱们经过java.nio中的channel替代了咱们上面的socket和fileInputStream,从而完成了咱们的零拷贝。

上面具体过程以下:

  1. 调用sendfie(),CPU下发指令叫DMA将磁盘数据拷贝到内核buffer中。
  2. DMA拷贝完成发出中断请求,进行CPU拷贝,拷贝到socket buffer中。sendFile调用完成返回。 3.DMA将socket buffer拷贝至网卡buffer。

能够看见咱们根本没有把数据复制到咱们的应用缓存中,因此这种方式就是零拷贝。可是这种方式依然很蛋疼,虽然减小到了只有三次数据拷贝,可是仍是须要CPU中断复制数据。为啥呢?由于DMA须要知道内存地址我才能发送数据啊。因此在Linux2.4内核中作了改进,将Kernel buffer中对应的数据描述信息(内存地址,偏移量)记录到相应的socket缓冲区当中。 最终造成了下面的过程: ![](https://user-gold-cdn.xitu.io/2018/8/2/164f669eaa58b005?w=1094&h=862&f=png&s=88495

这种方式让CPU全程不参与拷贝,所以效率是最好的。

在第三方开源框架中Netty中都有相似的代码,你们若是感兴趣能够下来自行搜索。

mmap映射

上面咱们提到了零拷贝的实现,可是咱们只能将数据原封不动的发给用户,并不能本身使用。因而Linux提供的一种访问磁盘文件的特殊方式,能够将内存中某块地址空间和咱们要指定的磁盘文件相关联,从而把咱们对这块内存的访问转换为对磁盘文件的访问,这种技术称为内存映射(Memory Mapping)。 咱们经过这种技术将文件直接映射到用户态的内存地址,这样对文件的操做再也不是write/read,而是直接对内存地址的操做。

在Java中依靠MappedByteBuffer进行mmap映射,具体的MappedByteBuffer能够详情参照这篇文章:https://www.jianshu.com/p/f90866dcbffc

最后

自此,零拷贝的神秘面纱也被揭盖,零拷贝只是为了减小CPU的占用,让CPU作更多真正业务上的事。经过这篇文章,你们能够本身下来看看Netty是怎么作零拷贝的相信将会有更加深入的印象。

因为做者本人水平不够,若是有什么错误,还请指正。若是上面问题有什么疑问的话能够关注公众号,来和我一块儿讨论吧,关注便可免费领取海量最新java学习资料视频,以及最新面试资料。

若是你们以为这篇文章对你有帮助,或者你有什么疑问想提供1v1免费vip服务,均可以关注个人公众号,关注便可免费领取海量最新java学习资料视频,以及最新面试资料,你的关注和转发是对我最大的支持,O(∩_∩)O:

相关文章
相关标签/搜索