I/O Zero Copy是什么?看完这篇你绝对会了
liebian365 2024-10-26 13:02 18 浏览 0 评论
前文我们介绍了 Java I/O 的底层原理,想必大家都知道类似 Netty、KafKa 等大数据量高吞吐框架都会提到一个概念 Zero Copy(零拷贝),这是什么技术呢,今天我们来学习下。
一、为什么需要 Zero Copy技术?
要想了解 zero-copy 我们需要知道该技术的应用场景,网络传输中一个基本的场景是:通过网络传输一个文件,按照一般的思路,用Java语言来描述发送端的逻辑,大致如下。
Socket socket = new Socket(HOST, PORT);
InputStream inputStream = new FileInputStream(FILE_PATH);
OutputStream outputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
while (inputStream.read(buffer) >= 0) {
outputStream.write(buffer);
}
看起来是很简单的,但是如果我们深入到操作系统的层面,就会发现实际的微观操作要更复杂。在这个场景中,至少出现 4 次数据拷贝和 3 次的内核态和用户态的切换。具体来说有以下步骤:
1、JVM 发出 read() 系统调用,触发上下文切换,从用户态切换到内核态。第一次 copy 是通过 DMA 引擎直接从硬盘文件系统读取文件内容存储在内核缓存空间。
2、将数据从内核缓冲区拷贝到用户空间缓冲区,read() 系统调用返回,并从内核态切换回用户态。
3、JVM发出 write() 系统调用,触发上下文切换,从用户态切换到内核态,将数据从用户缓冲区拷贝到内核中与目的地 Socket 关联的缓冲区。
4、数据最终经由 Socket 通过 DMA 传送到硬件(如网卡)缓冲区,write() 系统调用返回,并从内核态切换回用户态。
我们都知道,上下文切换是 CPU 密集型的工作,数据拷贝是 I/O 密集型的工作(至于为啥有内核缓冲与进程缓冲区,可以看这篇文章《10分钟看懂 Java IO 底层原理》)。如果一次简单的传输就要像上面这样复杂的话,效率是相当低下的。Zreo Copy (零拷贝)机制的终极目标,就是消除冗余的上下文切换和数据拷贝,提高效率。
二、Zero Copy 原理
通过上面的分析可以看出,第 2、3 次拷贝(也就是从内核空间到用户空间的来回复制)是没有意义的,数据应该可以直接从内核缓冲区直接送入 Socket 缓冲区。Zero Copy这个技术就是来解决这个问题,不过零拷贝需要由操作系统直接支持,不同操作系统有不同的实现方法。
关于零拷贝提供了两种解决方式:mmap + write 方式、sendfile 方式
1、虚拟内存
所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,这样做的好处就是:
1)多个虚拟内存可以指向同一个物理地址
2)虚拟内存空间可以远远大于物理内存空间
利用第一条特性可以优化一下上面的设计思路,就是把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样就不需要来回复制了:
2、mmap+write方式
使用 mmap+write 方式替换原来的传统 IO 方式,就是利用了虚拟内存的特性。mmap 是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系;这样就可以省掉原来内核 Read Buffer copy 数据到用户缓冲区,但是还是需要内核 Read Buffer 将数据 copy 到内核 Socket Buffer,如下图:
整体流程的核心区别就是,把数据读取到内核缓冲区后,应用程序进行写入操作时,直接是把内核 Read Buffer 的数据复制到 Socket Buffer 以便进行写入,这次内核之间的复制也是需要 CPU 参与的。
这个流程就少了一个CPU Copy,提升了 IO 的速度。不过发现上下文的切换还是 4 次,没有减少,因为还是要应用程序发起 write 操作。那能不能减少上下文切换呢?
3、sendfile方式
为了简化用户接口,同时减少 CPU 的拷贝次数,Linux 在版本 2.1 中引入了 sendfile() 这个系统调用。通过 sendfile 传送文件只需要一次系统调用,当调用 sendfile 时:
1)首先(通过 DMA )将数据从磁盘读取到内核 Read Buffer 中;
2)然后将内核 Read Buffer 的数据拷贝到 Socket buffer 中;
3)最后将 Socket buffer 中的数据 copy 到网卡设备中发送;
到这里就只有 3 次 Copy,其中只有 1 次 CPU Copy;3 次上下文切换。那能不能把CPU Copy减少到没有呢?
Linux2.4内核进行了优化,提供了gather操作,这个操作可以把最后一次CPU Copy去除,什么原理呢?就是在内核空间 Read Buffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制(其实本质就是和虚拟内存的解决方法思路一样,就是内存地址的记录)
三、Java Zero Copy
Java 的 Zero Copy 是由 Java NIO 来提供的,NIO 三大核心要素 :Buffer(缓冲区)、Channel(通道)和 Selector(选择器),Buffer 和Channel 组合实现了Java 的 Zero Copy,主要是由 MappedByteBuffer、DirectByteBuffer 以及 FileChannel来完成的。
- MappedByteBuffer
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,它继承自 ByteBuffer。FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。
map() 方法是 java.nio.channels.FileChannel 的抽象方法,由子类 FileChannelImpl实现,下面是和内存映射相关的核心代码:
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 {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
throw new IOException("Map failed", y);
}
}
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);
}
}
map() 方法通过本地方法 map0() 为文件分配一块虚拟内存,作为它的内存映射区域,然后返回这块内存映射区域的起始地址。
1)文件映射需要在 Java 堆中创建一个 MappedByteBuffer 的实例。如果第一次文件映射导致 OOM,则手动触发垃圾回收,休眠 100ms 后再尝试映射,如果失败则抛出异常。
2)通过 Util 的 newMappedByteBuffer方法或者 newMappedByteBufferR方法反射创建一个 DirectByteBuffer 实例,其中 DirectByteBuffer 是 MappedByteBuffer 的子类。
map() 方法返回的是内存映射区域的起始地址,通过(起始地址 + 偏移量)就可以获取指定内存的数据。这样一定程度上替代 read() 或 write() 方法,底层直接采用 Unsafe 类的 getByte() 和 putByte() 方法对数据进行读写。
- DirectByteBuffer
DirectByteBuffer 继承于 MappedByteBuffer ,DirectByteBuffer 的对象引用位于 Java 内存模型的堆里面,JVM 可以对 DirectByteBuffer 的对象进行内存分配和回收管理,一般使用 DirectByteBuffer 的静态方法 allocateDirect() 创建 DirectByteBuffer 实例并分配内存。
DirectByteBuffer 内部的字节缓冲区位在于堆外的(用户态)直接内存,它是通过 Unsafe 的本地方法 allocateMemory() 进行内存分配,底层调用的是操作系统的 malloc() 函数。
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
除此之外,初始化 DirectByteBuffer 时还会创建一个 Deallocator 线程,并通过 Cleaner 的 freeMemory() 方法来对直接内存进行回收操作,freeMemory() 底层调用的是操作系统的 free() 函数。
- FileChannel
FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。
transferTo() 和 transferFrom() 方法的底层实现是由 FileChannelImpl 提供的,底层原理是基于 sendfile 实现数据传输的。
以 transferTo() 的源码实现为例。FileChannelImpl 首先执行 transferToDirectly() 方法,以 sendfile 的零拷贝方式尝试数据拷贝。如果系统内核不支持 sendfile,进一步执行 transferToTrustedChannel() 方法,以 mmap 的零拷贝方式进行内存映射,这种情况下目的通道必须是 FileChannelImpl 或者 SelChImpl 类型。如果以上两步都失败了,则执行 transferToArbitraryChannel() 方法,基于传统的 I/O 方式完成读写,具体步骤是初始化一个临时的 DirectBuffer,将源通道 FileChannel 的数据读取到 DirectBuffer,再写入目的通道 WritableByteChannel 里面。
public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
// 计算文件的大小
long sz = size();
// 校验起始位置
if (position > sz)
return 0;
int icount = (int)Math.min(count, Integer.MAX_VALUE);
// 校验偏移量
if ((sz - position) < icount)
icount = (int)(sz - position);
long n;
if ((n = transferToDirectly(position, icount, target)) >= 0)
return n;
if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
return n;
return transferToArbitraryChannel(position, icount, target);
}
小结
本文开篇详述了为什么需要 Zero Copy以及其底层原理。从源码着手分析了 Java NIO 对零拷贝的实现,主要包括基于内存映射(mmap)方式的 MappedByteBuffer 以及基于 sendfile 方式的 FileChannel。
PS:这个坑已经越挖越大了,在这里又引入了虚拟内存、mmap 以及 DMA (Direct Memory Access),甚至 Java 的 NIO 等概念。
挖坑序列文章
相关推荐
- 4万多吨豪华游轮遇险 竟是因为这个原因……
-
(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...
- “菜鸟黑客”必用兵器之“渗透测试篇二”
-
"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...
- 科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白
-
作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...
- 麦子陪你做作业(二):KEGG通路数据库的正确打开姿势
-
作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...
- 知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势
-
智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...
- 每日新闻播报(September 14)_每日新闻播报英文
-
AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...
- 香港新巴城巴开放实时到站数据 供科技界研发使用
-
中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...
- 5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper
-
本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...
- Qt动画效果展示_qt显示图片
-
今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...
- 如何从0到1设计实现一门自己的脚本语言
-
作者:dong...
- 三年级语文上册 仿写句子 需要的直接下载打印吧
-
描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...
- C++|那些一看就很简洁、优雅、经典的小代码段
-
目录0等概率随机洗牌:1大小写转换2字符串复制...
- 二年级上册语文必考句子仿写,家长打印,孩子照着练
-
二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)