APP下载

走进科学之揭开神秘的零拷贝

消息来源:baojiabao.com 作者: 发布时间:2024-05-18

报价宝综合消息走进科学之揭开神秘的零拷贝

原文来源:https://github.com/javagrowing/JGrowing

前言

"零拷贝"这三个字,想必大家多多少少都有听过吧,这个技术在各种开源元件中都使用了,比如kafka,rocketmq,netty,nginx等等开源框架都在其中引用了这项技术。所以今天想和大家分享一下有关于零拷贝的一些知识。

计算机中资料传输

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

早期阶段:

分散连线,序列工作,程式查询。 在这个阶段,CPU就像个保姆一样,需要手把手的把资料从I/O界面读出然后再送给主存。

这个阶段具体流程是:

CPU主动启动I/O装置然后CPU一直问I/O装置:老铁你准备好了吗,注意这里是一直询问。如果I/O装置告诉了CPU说:我准备好了。CPU就从I/O界面中读资料。然后CPU又继续把这个资料传给主存,就像快递员一样。这种效率很低资料传输过程一直占据着CPU,CPU不能做其他更有意义的事。

界面模组和DMA阶段

界面模组

在冯诺依曼结构中,每个部件之间均有单独连线,不仅线多,而且导致扩充套件I/O装置很不容易,我们上面的早期阶段就是这个体系,叫作分散连线。扩充套件一个I/O装置得连线很多线。所以引入了总线连线方式,将多个装置连线在同一组总线上,构成装置之间的公共传输通道。

这个也是现在我们家用电脑或者一些小型计算器的资料交换结构。

在这种模式下资料交换采用程式中断的方式,我们上面知道我们启动I/O装置之后一直在轮询问I/O装置是否准备好,要是把这个阶段去掉了就好了,程式中断很好的实现了我们的夙愿:

CPU主动启动I/O装置。CPU启动之后不需要再问I/O,开始做其他事,类似异步化。I/O准备好了之后,通过总线中断告诉CPU我已经准备好了。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。

CPU发指令给I/O装置的DMA,由DMA将我们磁盘中的资料传输到核心空间的核心buffer。第二阶段触发我们的CPU中断,CPU开始将将资料从kernel buffer拷贝至我们的应用快取CPU将资料从应用快取拷贝到核心中的socket buffer.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,从而完成了我们的零拷贝。

上面具体过程如下:

呼叫sendfie(),CPU下发指令叫DMA将磁盘资料拷贝到核心buffer中。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对映,具体的MappedByteBuffer可以详情参照这篇文章:https://www.jianshu.com/p/f90866dcbffc 。

最后

自此,零拷贝的神秘面纱也被揭盖,零拷贝只是为了减少CPU的占用,让CPU做更多真正业务上的事。通过这篇文章,大家可以自己下来看看Netty是怎么做零拷贝的相信将会有更加深刻的印象。
2019-09-04 04:50:00

相关文章