零拷贝的基本原理及使用Java通过零拷贝实现数据传输
零拷贝,零开销,更高效!
零拷贝本身是一种思想,不与任何编程语言绑定,不懂Java的读者可以跳过零拷贝技术在Java中实现的具体细节。
许多Web应用提供大量的静态内容,主要就是从磁盘读取数据然后将数据写回套接字,中间不涉及数据的变换。这种操作对CPU的使用相对较少,但是效率很低:首先,内核从文件读取数据,然后将数据从内核空间拷贝到用户进程空间,最后应用程序将数据拷贝回内核空间并通过套接字发送。实际上,在整个流程中应用程序仅充当一个将数据从磁盘拷贝到套接字的低效中间层。
每次数据跨越用户态和内核态的边界,数据都需要拷贝,拷贝操作消耗CPU和内存带宽。幸运的是通过一种称为“零拷贝”的技术可消除这些不必要的拷贝。使用零拷贝的应用要求内核将磁盘数据直接拷贝到套接字而不再经过应用。零拷贝可以极大的提高应用的性能并减少上下文在内核态和用户态之间的切换次数。
在 Linux 和 Unix 系统中 Java 类库通过java.nio.channels.FileChannel
的transgerTo
方法支持零拷贝。可以使用transgerTo
方法在两个通道之间直接传递数据,而不要求数据经过应用程序。为了更好的理解零拷贝技术对性能的提升,首先通过传统复制语义实现一个简单文件传输功能,然后通过零拷贝技术实现同样功能,并比较两种实现在性能上的差异。
数据传输: 传统语义
考虑这样的场景:从文件读取数据并通过网络将数据传递给其他程序(这是很多应用的行为,包括提供静态内容的Web应用,FTP 服务器,邮件服务器等)。两个核心的操作如代码1所示:
代码 1. 从文件拷贝数据到套接字
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
虽然代码1非常的简单,但是在代码内部实现,拷贝操作需要上下文在用户态和内核态切换四次,在操作完成前数据需要拷贝四次。图1展示了数据如何从文件转移到套接字:
数据拷贝路径
图 1. 传统数据拷贝方法
图 2 展示了上下文切换:
上下文切换
图 2. 传统方法下的上下文切换
涉及的步骤包括:
read()
调用导致上下文从用户态切换到内核态。内核通过sys_read()
(或等价的方法)从文件读取数据。DMA引擎执行第一次拷贝:从文件读取数据并存储到内核空间的缓冲区。请求的数据从内核的读缓冲区拷贝到用户缓冲区,然后
read()
方法返回。read()
方法返回导致上下文从内核态切换到用户态。现在待读取的数据已经存储在用户空间内的缓冲区。send()
调用导致上下文从用户态切换到内核态。第三次拷贝数据从用户空间重新拷贝到内核空间缓冲区。但是,这一次,数据被写入一个不同的缓冲区,一个与目标套接字相关联的缓冲区。send()
系统调用返回导致第四次上下文切换。当DMA引擎将数据从内核缓冲区传输到协议引擎缓冲区时,第四次拷贝是独立且异步的。
使用中间内核缓冲区(而不是将数据直接发送到用户缓冲区)似乎非常低效。但是,进程引入中间内核缓冲区可以提高性能。在读取端使用中间内核缓冲区,在应用请求的数据没有超出内核缓冲区的数据时,内核缓冲区可以担当“预读缓存”的角色。在写端,中间内核缓冲区使写操作完全异步化。
不幸的是,当请求的数据大于内核缓冲区大小时这种方法往往会成为性能瓶颈。数据在最终被发送之前,在磁盘,内核缓冲区和用户缓冲区之间发生多次拷贝。零拷贝通过减少不必要的数据拷贝以提供性能。
数据传输: 零拷贝方式
如果你回想使用传统语义传递数据的场景,你会发现第二次和第三次数据拷贝并不是真的需要。应用程序除了缓存数据然后将数据传回套接字缓冲区外没有做任何事情。数据可以直接从内核的读缓冲区传输到套接字缓冲区。transferTo
方法允许你实现这样的流程。transferTo
方法的签名如代码 2所示:
代码 2. The transferTo() method
public void transferTo(long position, long count, WritableByteChannel target);
transferTo
方法将数据从文件通道传输到给定的可写字节通道。transferTo
内部实现依赖底层操作系统对零拷贝的支持:在UNIX和各 Linux 版本中,transgerTo
方法调用最终会调用sendfile()方法
,代码如 List 3 所示,sendfile
将数据从一个文件描述符传输到另一个:
代码 3. The sendfile() system call
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
代码 1 中的file.read()
和socket.send()
两个方法调用可以替换为一个transferTo()
方法调用,如代码 4所示:
代码 4. 使用 transferTo() 从磁盘拷贝数据到套接字
transferTo(position, count, writableChannel);
图 3 展示了使用 transferTo()
方法时,数据的流向:
数据拷贝路径
图 3. 使用transferTo()时数据拷贝
图 4 展示了使用 transferTo()
方法时,上下文的切换:
上下文切换
图 4. 使用 transferTo() 时上下文切换
使用transgerTo()
方法时涉及的步骤包括以下两步:
transgerTo
方法调用触发DMA引擎将文件上下文信息拷贝到内核读缓冲区,接着内核将数据从内核缓冲区拷贝到与外出套接字相关联的缓冲区。DMA引擎将数据从内核套接字缓冲区传输到协议引擎(第三次数据拷贝)。
这是一个改进:上下文切换的次数从4次减少到2次,数据拷贝的次数从4次减少到3次(仅有一次数据拷贝消耗CPU资源)。然而,这并没有实现零拷贝的目标,如果底层网卡支持gather operations,可以进一步减少内核拷贝数据的次数。Linux 内核 从2.4 版本开始修改了套接字缓冲区描述符以满足这个要求。这种方法不仅减少了多个上下文切换,还消除了消耗CPU的重复数据拷贝。用户使用的方法没有任何变化,依然通过transferTo
方法,但是方法的内部实现
发生了变化:
transferTo
方法调用触发 DMA 引擎将文件上下文信息拷贝到内核缓冲区。数据不会被拷贝到套接字缓冲区,只有数据的描述符(包括数据位置和长度)被拷贝到套接字缓冲区。DMA 引擎直接将数据从内核缓冲区拷贝到协议引擎,这样减少了最后一次需要消耗CPU的拷贝操作。
图 5 展示了在有gather option
条件下使用transferTo
时数据拷贝情况:
gather opreation 数据拷贝
图 5. 使用 transferTo() and gather operations 时的数据拷贝
性能测试
现在让我们在一个需要在客户端和服务器之间传输文件的程序中应用零拷贝。 TraditionalClient.java
和 TraditionalServer.java
基于传统复制语义,使用 File.read()
和 Socket.send()
方法读取和发送数据. TraditionalServer.java
是一个监听在5230端口等待客户端连接的服务器应用,每次从套接字中读取4kb的数据。 TraditionalClient.java
连接到服务器, 使用File.read()
方法每次从文件读取4kb数据,然后调用方法socket.send()
) 将数据通过套接字发送给服务器.
类似的, TransferToServer.java
和 TransferToClient.java
通过使用transferTo()
(使用sendfile()
系统调用发送数据)实现一样的将数据从客户端发送到服务器的功能。
性能比较
在Linux 内核2.6版本上,以毫秒统计使用传统方法和使用transferTo
方法传输不同大小的文件的耗时。表1展示了测试结果:
表 1. 性能标胶: 传统方法 vs. 零拷贝
File size | Normal file transfer (ms) | transferTo (ms) |
---|---|---|
7MB | 156 | 45 |
21MB | 337 | 128 |
63MB | 843 | 387 |
98MB | 1320 | 617 |
200MB | 2124 | 1150 |
350MB | 3631 | 1762 |
700MB | 13498 | 4422 |
1GB | 18399 | 8537 |
从测试结果来看使用transgerTo
的API和传统方法相比可以降低65%的传输时间。这可以有效的提高在不同I/O通道之间大量拷贝数据应用的性能。
Java的实现
NIO的零拷贝
File file = new File("test.zip");RandomAccessFile raf = new RandomAccessFile(file, "rw");FileChannel fileChannel = raf.getChannel();SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));// 直接使用了transferTo()进行通道间的数据传输fileChannel.transferTo(0, fileChannel.size(), socketChannel);
NIO的零拷贝由transferTo()方法实现。transferTo()方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在内部实现中,由native方法transferTo0()来实现,它依赖底层操作系统的支持。在UNIX和Linux系统中,调用这个方法将会引起sendfile()系统调用。
使用场景一般是:
- 较大,读写较慢,追求速度
- M内存不足,不能加载太大数据
- 带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小
以上都建立在不需要进行数据文件操作的情况下,如果既需要这样的速度,也需要进行数据操作怎么办?
那么使用NIO的直接内存!
NIO的直接内存
File file = new File("test.zip");RandomAccessFile raf = new RandomAccessFile(file, "rw");FileChannel fileChannel = raf.getChannel();MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
首先,它的作用位置处于传统IO(BIO)与零拷贝之间,为何这么说?
- IO,可以把磁盘的文件经过内核空间,读到JVM空间,然后进行各种操作,最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操作。
- 零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。由于它没有读取文件数据到JVM这一环,因此程序无法操作该文件数据,尽管效率很高!
而直接内存则介于两者之间,效率一般且可操作文件数据。直接内存(mmap技术)将文件直接映射到内核空间的内存,返回==一个操作地址(address)==,它解决了文件数据需要拷贝到JVM才能进行操作的窘境。而是直接在内核空间直接进行操作,省去了内核空间拷贝到用户空间这一步操作。
NIO的直接内存是由==MappedByteBuffer==实现的。核心即是map()方法,该方法把文件映射到内存中,获得内存地址addr,然后通过这个addr构造MappedByteBuffer类,以暴露各种文件操作API。
由于MappedByteBuffer申请的是堆外内存,因此不受Minor GC控制,只能在发生Full GC时才能被回收。而==DirectByteBuffer==改善了这一情况,它是MappedByteBuffer类的子类,同时它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。因此它既可以通过Full GC来回收内存,也可以调用clean()方法来进行回收。
另外,直接内存的大小可通过jvm参数来设置:-XX:MaxDirectMemorySize。
NIO的MappedByteBuffer还有一个兄弟叫做HeapByteBuffer。顾名思义,它用来在堆中申请内存,本质是一个数组。由于它位于堆中,因此可受GC管控,易于回收。
总结
我们已经证明了在从一个通道读取数据并将相同的数据写入另一个通道的场景下使用transferTo
带来的巨大性能优势。内部缓冲区的拷贝,尽管这些拷贝隐藏在内核里,但是也是可观的消耗。对于需要处理在通道之间拷贝大量数据的应用,零拷贝技术可以显著的提升性能。
扩展阅读
在Java编程领域,Netty是一个非常流行的基于事件驱动的异步网络应用框架,Netty的核心框架之一就是拥有丰富的支持零拷贝的字节缓冲区,想进一步了解零拷贝技术的朋友可以深入研究Netty中零拷贝技术的实现。
零拷贝的基本原理及使用Java通过零拷贝实现数据传输相关推荐
- java访问修饰符详解——学java,零基础不怕,不只要理论,更要实践+项目,a href=http://www.bjweixin.com太原维信科技提供 /a...
java访问修饰符详解--学java,零基础不怕,不只要理论,更要实践+项目 <a href=http://www.bjweixin.com>太原维信科技提供 </a> pub ...
- 多层数组如何遍历_带你从零学大数据系列之Java篇---第五章:数组
温馨提示:如果想学扎实,一定要从头开始看凯哥的一系列文章(凯哥带你从零学大数据系列),千万不要从中间的某个部分开始看,知识前后是有很大关联,否则学习效果会打折扣. 系列文章第一篇是拥抱大数据:凯哥带你 ...
- Java培训零基础学员必须要知道的知识点
学习java那么遇到的知识点有很多,很多同学都会问到一些关于java的编程知识点,下面小编就为大家整理一下java培训零基础学员必须要知道的6个知识点. Java培训零基础学员必须要知道的6个知识点: ...
- 零基础如何选择适合的Java培训课程
很多人都想要学习java技术,但是害怕自己是零基础学不好,所以想要找专业的java培训机构进行学习,但是零基础如何选择适合的Java培训课程成了他们比较头疼的事情,下面小编就为大家做下详细的介绍. ...
- map根据value值排序_凯哥带你从零学大数据系列之Java篇---第十九章:集合(Map+Collections)...
温馨提示:如果想学扎实,一定要从头开始看凯哥的一系列文章(凯哥带你从零学大数据系列),千万不要从中间的某个部分开始看,知识前后是有很大关联,否则学习效果会打折扣. 系列文章第一篇是拥抱大数据:凯哥带你 ...
- ios 获取一个枚举的所有值_凯哥带你从零学大数据系列之Java篇---第十一章:枚举...
温馨提示:如果想学扎实,一定要从头开始看凯哥的一系列文章(凯哥带你从零学大数据系列),千万不要从中间的某个部分开始看,知识前后是有很大关联,否则学习效果会打折扣. 系列文章第一篇是拥抱大数据:凯哥带你 ...
- java 将Map拷贝到另一个Map对象当中
java 将Map拷贝到另一个Map对象当中 CreateTime--2018年6月4日09点46分 Author:Marydon 1.需求说明 将一个MapA对象中所有的键值对完全拷贝到另一个Map ...
- 大剑无锋之Java的深浅拷贝解释一下!
拷贝的一个经典的使用场景:当前对象要传给其他多个方法使用,如果该对象在某一个方法中被修改,那么这个修改会影响到其他方法. 如果要避免这种影响,就需要给每一个方法都传入一个当前对象的拷贝. 深与浅拷贝的 ...
- java 实例对象拷贝,实例详解java对象拷贝
这篇文章主要介绍了java对象拷贝详解及实例的相关资料,需要的朋友可以参考下 java对象拷贝详解及实例 Java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是无法达到目的的:@T ...
- Java io流---拷贝文件夹下的所有文件和目录
Java io流-拷贝文件夹下的所有文件和目录 代码: package demo01;import java.io.*; import java.util.TreeMap;public class C ...
最新文章
- ORACLE 创建作业JOB例子
- python绘图实例-Python使用matplotlib简单绘图示例
- c语言数组数据的输入,在C语言中,数组中的值如何输入到函数中?
- 【Git】本地仓库上传到github免密操作
- python to_excel保存成xls_pd.ExcelWriter(to_excel)保存结果到已存在的excel文件中
- 2014年 第5届 蓝桥杯 Java B组 省赛解析及总结
- 手动加支付宝遇到的错误--iOS
- HTTP1.1/2.0与QUIC协议
- c 数组上限_高级I/O复用技术:Epoll的使用及一个完整的C实例含代码
- (转载)valgrind,好东西,一般人我不告诉他~~ 选项
- 基于系统的流量控制(Qos)
- 数学建模题型及其常用算法
- Typora下载安装教程
- 16进制颜色码对照表
- FileSplit:文件的子集--文件分割体
- 史上最优美的Android原生UI框架XUI使用指南
- 《UnityAPI.Rect矩阵》(Yanlz+Unity+SteamVR+云技术+5G+AI+VR云游戏+Rect+Contains+Overlaps+ToString+立钻哥哥++OK++)
- AlphaCode 惊世登场!编程版“阿法狗”悄悄参赛,击败一半程序员
- 【Hack The Box】windows练习-- Silo
- SMSAlarm短信猫语音猫快速连接