前言
从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能。
这个词我们也经常在 Java NIO、Netty、Kafka、RocketMQ 等框架中听到。经常作为其提升性能的一大亮点。
下面从 I/O 的几个概念开始,进而在分析零拷贝。
缓冲区是所有 I/O 的基础。
I/O 讲的无非就是把数据移进或移出缓冲区。进程执行 I/O 操作,就是向操作系统发出请求。让它要么把缓冲区的数据排干(写),要么填充缓冲区(读)。
下面看一个 Java 进程发起 read 请求加载数据大致的流程图:
进程发起 read 请求之后,内核接收到 read 请求会先检查内核空间中是否已经存在进程所需要的数据:
如果进程发起 write 请求,同样需要把用户缓冲区里面的数据 copy 到内核的 Socket 缓冲区里面。然后再通过 DMA 把数据 copy 到网卡中,发送出去。
你可能觉得这样挺浪费空间的,每次都需要把内核空间的数据拷贝到用户空间中,所以零拷贝的出现就是为了解决这种问题的;
关于零拷贝提供了两种方式分别是:mmap+write 方式,sendfile 方式;
所有现代操作系统都使用虚拟内存。使用虚拟的地址取代物理地址的好处是:
利用第 1 条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址。这样,DMA 就可以填充对内核和用户空间进程同时可见的缓冲区了。
过程大致如下图所示:
省去了内核与用户空间的往来拷贝,Java 也利用操作系统的此特性来提升性能,下面重点看看 Java 对零拷贝都有哪些支持。
使用 mmap+write 方式代替原来的 read+write 方式。
mmap 是一种内存映射文件的方法。即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
这样就可以省掉原来内核 read 缓冲区 copy 数据到用户缓冲区,但是还是需要内核 read 缓冲区将数据 copy 到内核 Socket 缓冲区。
过程大致如下图所示:
sendfile 系统调用在内核版本 2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。
sendfile 系统调用的引入不仅减少了数据复制,还减少了上下文切换的次数。
大致如下图所示:
数据传送只发生在内核空间,所以减少了一次上下文切换,但是还是存在一次 copy。
能不能把这一次 copy 也省略掉?
Linux2.4 内核中做了改进,将 Kernel buffer 中对应的数据描述信息(内存地址、偏移量)记录到相应的 Socket 缓冲区当中。
这样连内核空间中的一次 CPU copy 也省掉了。
Java NIO 提供的 FileChannel 提供了 map() 方法,该方法可以在一个打开的文件和 MappedByteBuffer 之间建立一个虚拟内存映射。
MappedByteBuffer 继承于 ByteBuffer,类似于一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘的一个文件中。调用 get() 方法会从磁盘中获取数据,此数据反映该文件当前的内容。调用 put() 方法会更新磁盘上的文件,并且对文件做的修改对其他阅读者也是可见的。
下面看一个简单的读取实例,然后在对 MappedByteBuffer 进行分析:
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file = new File("D://db.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
}
}
主要通过FileChannel提供的map()来实现映射,map()方法如下:
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
分别提供了三个参数,MapMode、Position 和 size;分别表示:
重点看一下 MapMode,请两个分别表示只读和可读可写。
当然,请求的映射模式受到 Filechannel 对象的访问权限限制。如果在一个没有读权限的文件上启用 READ_ONLY,将抛出 NonReadableChannelException。
PRIVATE 模式表示写时拷贝的映射,意味着通过 put() 方法所做的任何修改都会导致产生一个私有的数据拷贝,并且该拷贝中的数据只有 MappedByteBuffer 实例可以看到。
该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage collected),那些修改都会丢失。
下面概要地浏览一下 map() 方法的源码:
public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
// ...省略...
int pagePosition = (int)(position % allocationGranularity);
long mapPosition = position - pagePosition;
long mapSize = size + pagePosition;
try {
// If no exception was thrown from map0, the address is valid
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted memory
// so force gc and re-attempt map
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
// After a second OOME, fail
throw new IOException("Map failed", y);
}
}
// On Windows, and potentially other platforms, we need an open
// file descriptor for some mapping operations.
FileDescriptor mfd;
try {
mfd = nd.duplicateForMapping(fd);
} catch (IOException ioe) {
unmap0(addr, mapSize);
throw ioe;
}
assert (IOStatus.checkAll(addr));
assert (addr % allocationGranularity == 0);
int isize = (int)size;
Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
if ((!writable) || (imode == MAP_RO)) {
return Util.newMappedByteBufferR(isize,
addr + pagePosition,
mfd,
um);
} else {
return Util.newMappedByteBuffer(isize,
addr + pagePosition,
mfd,
um);
}
}
大致意思就是通过 native 方法获取内存映射的地址。
如果失败,手动 GC 再次映射;最后通过内存映射的地址实例化出 MappedByteBuffer。
MappedByteBuffer 本身是一个抽象类,其实这里真正实例化出来的是 DirectByteBuffer。
DirectByteBuffer 继承于 MappedByteBuffer,从名字就可以猜测出开辟了一段直接的内存,并不会占用 JVM 的内存空间。
上一节中通过 Filechannel 映射出的 MappedByteBuffer 其实际也是 DirectByteBuffer。
当然,除了这种方式,也可以手动开辟一段空间:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
如上开辟了100字节的直接内存空间。
经常需要从一个位置将文件传输到另外一个位置,FileChannel 提供了 transferTo() 方法用来提高传输的效率。
首先看一个简单的实例:
public class ChannelTransfer {
public static void main(String[] argv) throws Exception {
String files[]=new String[1];
files[0]="D://db.txt";
catFiles(Channels.newChannel(System.out), files);
}
private static void catFiles(WritableByteChannel target, String[] files) throws Exception
for (int i = 0; i < files.length; i++) {
FileInputStream fis = new FileInputStream(files[i]);
FileChannel channel = fis.getChannel();
channel.transferTo(0, channel.size(), target);
channel.close();
fis.close();
}
}
}
通过 FileChannel 的 transferTo() 方法将文件数据传输到 System.out 通道,接口定义如下:
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
几个参数也比较好理解,分别是开始传输的位置、传输的字节数、以及目标通道。
transferTo() 允许将一个通道交叉连接到另一个通道,而不需要一个中间缓冲区来传递数据。
注意:这里不需要中间缓冲区有两层意思。首先,不需要用户空间缓冲区来拷贝内核缓冲区。其次两个通道都有自己的内核缓冲区,两个内核缓冲区也可以做到无需拷贝数据。
Netty 提供了零拷贝的 buffer。在传输数据时,最终处理的数据会需要对单个传输的报文进行组合和拆分。
NIO 原生的 ByteBuffer 无法做到,Netty 通过提供的 Composite(组合)和 Slice(拆分)两种 buffer 来实现零拷贝。
看下面一张图会比较清晰:
TCP 层 HTTP 报文被分成了两个ChannelBuffer,这两个 Buffer 对我们上层的逻辑(HTTP 处理)是没有意义的。
但是两个 ChannelBuffer 被组合起来,就成为了一个有意义的 HTTP 报文,这个报文对应的 ChannelBuffer,才是能称之为 ”Message” 的东西,这里用到了一个词 ”Virtual Buffer”。
可以看一下 Netty 提供的 CompositeChannelBuffe r源码:
public class CompositeChannelBuffer extends AbstractChannelBuffer {
private final ByteOrder order;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;
private final boolean gathering;
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
// ...省略...
}
components 用来保存的就是所有接收到的 buffer,indices 记录每个 buffer 的起始位置,lastAccessedComponentId 记录上一次访问的 ComponentId。
CompositeChannelBuffer 并不会开辟新的内存并直接复制所有 ChannelBuffer 内容,而是直接保存了所有 ChannelBuffer 的引用,并在子 ChannelBuffer 里进行读写,实现了零拷贝。
RocketMQ 的消息采用顺序写到 commitlog 文件,然后利用 consume queue 文件作为索引;RocketMQ 采用零拷贝 mmap+write 的方式来回应 Consumer 的请求。
同样 Kafka 中存在大量的网络数据持久化到磁盘和磁盘文件通过网络发送的过程,Kafka 使用了 sendfile 零拷贝方式。
零拷贝如果简单用 Java 里面对象的概率来理解的话,其实就是使用的都是对象的引用,每个引用对象的地方对其改变就都能改变此对象,永远只存在一份对象。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/B-w1mdbOGwJPvUADhbS06Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。