前言

"零拷贝"这三个字,想必大家多多少少都有听过吧,这个技术在各种开源组件中都应用了,比方kafka,rocketmq,netty,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设施得连贯很多线。所以引入了总线连贯形式,将多个设施连贯在同一组总线上,形成设施之间的公共传输通道。

这个也是当初咱们家用电脑或者一些小型计算器的数据交换构造。

在这种模式下数据交换采纳程序中断的形式,咱们下面晓得咱们启动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和中断两种即可,上面介绍的都是用来适宜大型计算机的一些,这里只说简略的过一下:

具备通道构造的阶段

在小型计算机中采纳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缓冲区当中。 最终造成了上面的过程:

这种形式让CPU全程不参加拷贝,因而效率是最好的。

在第三方开源框架中Netty,RocketMQ,kafka中都有相似的代码,大家如果感兴趣能够下来自行搜寻。

mmap映射

下面咱们提到了零拷贝的实现,然而咱们只能将数据一成不变的发给用户,并不能自己应用。于是Linux提供的一种拜访磁盘文件的非凡形式,能够将内存中某块地址空间和咱们要指定的磁盘文件相关联,从而把咱们对这块内存的拜访转换为对磁盘文件的拜访,这种技术称为内存映射(Memory Mapping)。 咱们通过这种技术将文件间接映射到用户态的内存地址,这样对文件的操作不再是write/read,而是间接对内存地址的操作。

在Java中依附MappedByteBuffer进行mmap映射。

最初

自此,零拷贝的神秘面纱也被揭盖,零拷贝只是为了缩小CPU的占用,让CPU做更多真正业务上的事。通过这篇文章,大家能够本人下来看看Netty是怎么做零拷贝的置信将会有更加粗浅的印象。

因为作者自己程度不够,如果有什么谬误,还请斧正。如果下面问题有什么疑难的话能够退出我的Java老手学习群:3907814 ,来和我一起探讨吧,关注即可收费支付海量最新java学习材料视频,以及最新面试材料。