前言
“ 零拷贝 ” 这三个字,想必大家多多少少都有听过吧,这个技术在各种开源组件中都应用了,比方 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 设施是否筹备好,要是把这个阶段去掉了就好了,程序中断很好的实现了咱们的夙愿:
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 学习材料视频,以及最新面试材料。