当前位置: 首页 > 编程日记 > 正文

Netty 之 Zero-copy 的实现(下)

上一篇说到了 CompositeByteBuf ,这一篇接着上篇的讲下去。

FileRegion

让我们先看一个Netty官方的example

// netty-netty-4.1.16.Final\example\src\main\java\io\netty\example\file\FileServerHandler.java
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {RandomAccessFile raf = null;long length = -1;try {raf = new RandomAccessFile(msg, "r");length = raf.length();} catch (Exception e) {ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');return;} finally {if (length < 0 && raf != null) {raf.close();}}ctx.write("OK: " + raf.length() + '\n');if (ctx.pipeline().get(SslHandler.class) == null) {// SSL not enabled - can use zero-copy file transfer.ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));} else {// SSL enabled - cannot use zero-copy file transfer.ctx.write(new ChunkedFile(raf));}ctx.writeAndFlush("\n");
}

可以看到在没开启SSL的情况下handler是通过 DefaultFileRegion 类传输文件的,而 DefaultFileRegionFileRegion 接口的一个实现, FileRegion 的注释是这么写的:

A region of a file that is sent via a Channel which supports zero-copy file transfer.

FileRegion 内部封装了 Java NIO 的 FileChannel.transferTo() 方法,要了解 FileRegionZero-copy 的原理,我们得先了解 transferTo() 方法。

让我们看一段传输文件的一般写法吧。

File.read(file, buf, len);
Socket.send(socket, buf, len);

尽管上面的代码看起来很简单,但在内部实际包含了4次用户态-内核态上下文切换,和4次数据拷贝。

上下文切换示意图

数据拷贝示意图

其中步骤有:

  1. read() 调用导致了一次用户态到内核态的上下文切换,在内部,一个 sys_read() (或等价函数)被执行来从文件中读取数据。第一次拷贝是由 DMA 引擎将数据从磁盘文件存储到内核地址空间缓冲区。
  2. 被请求长度的数据从内核的读缓冲区拷贝到用户缓冲区,并且 read() 调用返回。这个返回导致又一次从内核态到用户态的上下文切换。现在数据是存储在用户地址空间缓冲区。
  3. send() 调用引起了一次从用户态到内核态的上下文切换。第三次拷贝又一次将数据放进内核地址空间缓冲区,尽管这一次是放进另一个不同的缓冲区,和目标socket联系在一起。
  4. send() 系统调用返回,产生了第四次上下文切换。第四次拷贝由 DMA 引擎独立异步地将数据从内核缓冲区传递给协议引擎。

看到这里可能有些读者会问,read() 函数为什么不直接将数据拷贝到用户地址空间的缓冲区,而要经内核地址空间的缓冲区转一次手,这不是白白多了一次拷贝操作吗?

对IO函数有了解的童鞋肯定知道,在IO函数的背后有一个缓冲区 buffer ,我们平常的读和写操作并不是直接和底层硬件设备打交道,而是通过一块叫缓冲区的内存区域缓存数据来间接读写。我们知道,和CPU、高速缓存、内存比,磁盘、网卡这些设备属于慢速设备,交换一次数据要花很多时间,同时会消耗总线传输带宽,所以我们要尽量降低和这些设备打交道的频率,而使用缓冲区中转数据就是为了这个目的。

引用参考文献[2]中的话:

Using the intermediate buffer on the read side allows the kernel buffer to act as a "readahead cache" when the application hasn't asked for as much data as the kernel buffer holds. This significantly improves performance when the requested data amount is less than the kernel buffer size. The intermediate buffer on the write side allows the write to complete asynchronously.

大意是说,在读一侧的中间缓冲区可以作为预读缓存显著提高当请求数据大小小于内核缓冲区大小时的读性能,在写一侧的中间缓冲区可以允许写操作异步完成。

不过,当读请求数据的大小大于内核缓冲区时这个策略本身会变成一个性能瓶颈,数据在到达应用程序前会在磁盘、内核缓冲区、用户缓冲区之间反复多次拷贝。

让我们重新思考下上面的过程,会发现第二次和第三次的拷贝其实是不必要的,我们为什么不直接从读缓冲区将数据传输到socket缓冲区呢?实际上这就是 transferTo() 所做的。

public void transferTo(long position, long count, WritableByteChannel target);

transferTo() 方法将数据从一个文件channel传输到一个可写channel。在内部它依赖于操作系统对 Zero-copy 的支持,在UNIX/Linux系统上, transferTo() 实际会调用 sendfile() 这个系统函数,将数据从一个文件描述符传输到另一个。

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

transferTo上下文切换

transferTo数据拷贝

可以看到我们将上下文切换已经从4次减少到2次,同时把数据拷贝从4次减少到3次(只有1次 CPU 参与,另外2次 DMA 引擎完成),那么我们可不可以把这唯一一次CPU参与的数据拷贝也省掉呢?

如果网卡支持 gather operations 内核就可以进一步减少数据拷贝。在 Linux kernels 2.4 及更新的版本,socket 描述符已经为适应这个需求做了变化。现在这个方法不仅减少了上下文切换,而且消除了CPU参与的数据拷贝。API接口是一样的,但是实质已经发生了变化:

  1. transferTo() 方法引起 DMA 引擎将文件内容拷贝到内核缓冲区。
  2. 没有数据从内核缓冲区拷贝到socket缓冲区,只有携带位置和长度信息的描述符被追加到socket缓冲区上, DMA 引擎直接将内核缓冲区的数据传递到协议引擎,全程无需CPU拷贝数据。

transferTo和gather operation数据拷贝

到这里大家对 transferTo() 实现 Zero-copy 的原理应该很清楚了吧, FileRegion 是对 transferTo() 的一个封装,所以也是一样的。

DirectByteBuffer

DirectByteBuffer 是 Java NIO 用于实现堆外内存的一个很重要的类,而 NettyDirectByteBuffer 作为PooledDirectByteBufUnpooledDirectByteBuf 的内部数据容器(区别于 HeapByteBuf 直接用 byte[] 作为数据容器),以使用和操纵堆外内存。要了解 DirectByteBuffer 怎么实现 Zero-copy,我们要先了解 DirectByteBuffer 这个类和堆外内存。

DirectByteBuffer继承关系

DirectByteBuffer 类本身还是位于Java内存模型的堆中,堆内存是JVM可以直接管控、操纵的内存,而 DirectByteBuffer 中的 unsafe.allocateMemory(size) 是一个native方法,这个方法分配的是堆外内存,通过 C 的 malloc 来进行分配的。分配的内存是在系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在 DirectByteBuffer 一定会存在某种方式操纵堆外内存。

DirectByteBuffer 的父类 Buffer 中有个 address 属性:

// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;

address 只会被直接缓存给使用到。之所以将 address 属性升级放在 Buffer 中,是为了在JNI调用 GetDirectBufferAddress 时提高效率。

address 表示分配的堆外内存的地址,JNI对这个堆外内存的操作都是通过这个 address 实现的。

在回答为什么堆外内存可以实现 Zero-copy 前,我们先要明确一个结论,那就是 操作系统不能直接访问Java堆的内存区域

JNI方法访问的内存区域是一个已经确定的内存区域,如果该内存地址指向的是一个Java堆内存的话,在操作系统正在访问这个内存地址时,JVM在这个时候进行了GC操作,GC经常会进行先标记再压缩的操作,即将可回收的空间做标记,然后清空标记位置的内存,然后会进行一个压缩,压缩会涉及到对象的移动,以腾出一块更加完整、连续的内存空间,以容纳更大的新对象,但是这个移动的过程会使JNI调用的数据错乱。

为了解决上述的问题,一般会做一个堆内存与堆外内存之间数据拷贝的操作:比如我们要完成一个从文件中读数据到堆内存的操作,即 FileChannelImpl.read(HeapByteBuffer) ,这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再将数据拷贝到堆内存,这样我们就读到了文件中的内容。

FileChannelImpl.read

static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {if (var1.isReadOnly()) {throw new IllegalArgumentException("Read-only buffer");} else if (var1 instanceof DirectBuffer) {return readIntoNativeBuffer(var0, var1, var2, var4);} else {// 分配临时的堆外内存ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());int var7;try {// File I/O 操作会将数据读入到堆外内存中int var6 = readIntoNativeBuffer(var0, var5, var2, var4);var5.flip();if (var6 > 0) {// 将堆外内存的数据拷贝到堆外内存中var1.put(var5);}var7 = var6;} finally {// 里面会调用DirectBuffer.cleaner().clean()来释放临时的堆外内存Util.offerFirstTemporaryDirectBuffer(var5);}return var7;}
}

而写操作则反之,我们会将堆内存的数据先写到堆外内存,然后操作系统会将堆外内存的数据写入到堆内存。

如果我们直接使用堆外内存,即直接在堆外分配一块内存来存储数据,这样就可以避免堆内存和堆外内存之间的数据拷贝,进行I/O操作时直接将堆外内存地址传给JNI的I/O函数就好了。

这里引用一段 stackoverflow 里关于 ByteBuffer.allocate() vs. ByteBuffer.allocateDirect() 的讨论:

Operating systems perform I/O operations on memory areas. These memory areas, as far as the operating system is concerned, are contiguous sequences of bytes. It's no surprise then that only byte buffers are eligible to participate in I/O operations. Also recall that the operating system will directly access the address space of the process, in this case the JVM process, to transfer the data. This means that memory areas that are targets of I/O perations must be contiguous sequences of bytes. In the JVM, an array of bytes may not be stored contiguously in memory, or the Garbage Collector could move it at any time. Arrays are objects in Java, and the way data is stored inside that object could vary from one JVM implementation to another.

这也是堆外内存 DirectByteBuffer 被引进的原因。

但是同时,创建和销毁一块堆外内存的花销要比堆内存昂贵得多,这是因为堆外内存的创建和销毁要通过系统相关的 native 方法,而不是在 Java 堆上直接由 JVM 操控。为了更有效地重用堆外内存,Netty 引入了内存池机制手动管理内存,这是一个 Java 版的 Jemalloc,后面有机会再写篇文章专门介绍这个,因为我现在也不是很懂(先挖个坑)。

总结

到这里关于 Netty 实现 Zero-copy 的4种机制,切片共用,组合缓冲区,操作系统层的零拷贝以及堆外内存已经介绍完了,因为本人也是最近刚开始学习 Netty 框架,对很多知识点掌握得还不是很通透,如果文章写得有什么不妥的地方还请大家不吝赐教。

参考

[1] 对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解
[2] Efficient data transfer through zero copy
[3] 堆外内存 之 DirectByteBuffer 详解

相关文章:

Java中final关键字如何使用?

final变量只能赋值一次&#xff0c;赋值的方式有三种&#xff1a; 1)声明变量时直接赋值; 2)非静态成员变量在{}块中赋值&#xff0c;静态成员变量在static{}块中赋值; 3)非静态成员变量在构造方法中赋值。 final修饰类 final类不能被继承&#xff0c;因此不会有子类。final类中…

技术图文:双指针在求解算法题中的应用

背景 前段时间&#xff0c;在知识星球立了一个Flag&#xff0c;这是总结Leetcode刷题的第三篇图文。 理论部分 Python list 的源码地址&#xff1a; https://github.com/python/cpython/blob/master/Include/listobject.h https://github.com/python/cpython/blob/master/O…

【CSON原创】HTML5游戏框架cnGameJS开发实录(外部输入模块篇)

返回目录 1.为什么我们需要外部输入模块&#xff1f; 在游戏中我们常常用到类似这样的操作&#xff1a;鼠标点击某位置&#xff0c;玩家对象移动到该位置&#xff0c;或者按鼠标方向键&#xff0c;玩家向不同方向移动&#xff0c;等等。这些操作无一不用与外部输入设备打交道。…

中国科协(深圳)海外人才离岸创新创业基地源创力中心开业,主打国际创业服务...

2017年9月28日&#xff0c;由深圳市科学技术协会主办、深圳市罗湖区人民政府支持&#xff0c;深圳市源创力离岸创新中心承办的“梧桐山基地开园仪式暨梧桐湾未来论坛”于深圳举办。 据介绍&#xff0c; “中国科协&#xff08;深圳&#xff09;海外人才离岸创新创业基地”是在深…

找java培训机构如何挑选

​ java技术在互联网行业的需求率还是非常高的&#xff0c;它的发展前景非常可观&#xff0c;想要学好java技术&#xff0c;那么寻找一个好的java培训机构是非常重要的&#xff0c;那么找java培训机构如何挑选呢?来看看下面的详细介绍。 ​  找java培训机构如何挑选? 在选择…

技术图文:集合技术在求解算法题中的应用

背景 前段时间&#xff0c;在知识星球立了一个Flag&#xff0c;这是总结Leetcode刷题的第四篇图文。 理论部分 HashSet C# 语言中 HashSet<T> 是包含不重复项的无序列表&#xff0c;称为“集合(set)”。由于set是一个保留字&#xff0c;所以用HashSet来表示。 public…

sql server 2008数据导入Oracle方法

试了几种sql server数据导入Oracle的方法&#xff0c;发现还是sql server 的导入导出工具最好使。使用方法很简单&#xff0c;照着向导做就可以。不过使用中需要注意以下几点&#xff1a; 系统盘需要足够大。因为SSIS的临时文件都是生成在系统盘的&#xff0c;系统盘太小&#…

nginx+tomcat配置负载均衡集群

一、Hello world 1、前期环境准备 准备两个解压版tomcat&#xff0c;如何同时启动两个tomcat&#xff0c;方法如下&#xff1a; 首先去apache tomcat官网下载一个tomcat解压版。 解压该压缩包&#xff0c;生成n份tomcat 分别命名为 tomcat1&#xff0c;tomcat2&#xff0c; 然后…

参加UI设计培训要学多久

​ UI设计要学习的内容有很多&#xff0c;至于参加UI设计培训要学多久这个问题&#xff0c;要看你的学习能力和所在的UI设计培训机构都教些什么&#xff0c;我们来看看下面的详细介绍。 参加UI设计培训要学多久?千锋教育的课程大纲分享给大家参考学习一下&#xff1a; 阶段一&…

技术图文:C# 语言中的扩展方法

背景 前段时间&#xff0c;在知识星球立了一个Flag&#xff0c;在总结 Leetcode 刷题的第五篇图文时遇到了扩展方法 这个知识点&#xff0c;于是先总结一下。 1&#xff0e;扩展方法概述 扩展方法能够向现有类型“添加”方法&#xff0c;而无需创建新的派生类型、重新编译或以…

如何在ToolBar中显示文字和图标,自定义图标大小,并和MenuItem关联

要注意以下几个方面,先后顺序未必正确,有可能多设几次 1.设置ToolBar可以显示文字ToolBar.ShowCaption : True;2.设置ToolButton大小ImageList.WidthImageList.Height3.设置菜单关联4.设置运行时显示图标(这个是关键)ToolButton.Menuitum.ImageIndex要保证MenuItem所在的MainMe…

C#程序调用cmd执行命令

酷小孩 原文 C#程序调用cmd执行命令 对于C#通过程序来调用cmd命令的操作&#xff0c;网上有很多类似的文章&#xff0c;但很多都不行&#xff0c;竟是漫天的拷贝。我自己测试整理了一下。 代码&#xff1a; string str Console.ReadLine();System.Diagnostics.Process p new …

Java虚拟机的内存空间有几种

Java虚拟机的内存空间有几种&#xff1f;(1)问题分析&#xff1a; JVM(虚拟机)的内存划分 不同的数据使用的是哪一块内存空间 (2)核心答案讲解&#xff1a; Java虚拟机有那几块内存空间&#xff1a; 1)栈内存&#xff1a;方法运行时所进入的内存&#xff0c;里面还会存储程序的…

技术图文:排序技术在求解算法题中的应用

背景 前段时间&#xff0c;在知识星球立了一个Flag&#xff0c;这是总结Leetcode刷题的第五篇图文。 理论部分 C# 中的排序 对集合类的排序&#xff0c;我们通常使用位于 System.Core 程序集&#xff0c;System.Linq命名空间下&#xff0c;Enumerable静态类中的扩展方法。 …

如果有电脑——计算机达人成长之路(36)

5、电脑情缘&#xff08;一&#xff09;王新华的电脑 现在的大学生一般都有一个工具&#xff0c;就是计算机&#xff0c;尤其是计算机科学系的学生&#xff0c;几乎人手一台。对此&#xff0c;木鸿飞只能深深的说上一句&#xff1a;“幸福啊&#xff01;” 现在人可能不能了解这…

Javascript中二进制数据处理方法

Javascript中二进制数据处理方法 转载于:https://www.cnblogs.com/motadou/archive/2012/02/19/2358514.html

正规Java培训机构是什么样的

​ 正规Java培训机构是什么样的?这对于很多想真正学习到java技术的人来说是非常重要的&#xff0c;选择一个适合自己的靠谱的Java培训机构&#xff0c;学有所成工作也是比较稳定的&#xff0c;下面我们来看看详细的介绍。 ​  正规Java培训机构是什么样的?其实对于这个问题…

《40期》 我们要把世纪末日变成重生日

2012年&#xff0e;传说中一个会是世纪末日的一年。&#xff08;ps&#xff1a;电影看多了……- _-!!!&#xff09;&#xff0c;但是寒假过后的北京。天气却是十分的晴朗、出奇的好。而就在今天也就是2012年2月9日40期的开班典礼就选了这一天。地点就是在育荣教学园区2栋教学楼…

LeetCode刷题宝典 V1.0 PDF下载

前段时间&#xff0c;在知识星球立了一个Flag&#xff0c;现在 Flag 的进度为 100%&#xff0c;很是开心。 为了大家学习的方便&#xff0c;所以整理了这份150多页的小册子。可以作为学习数据结构与算法或备考计算机类研究生的参考资料&#xff0c;希望对大家有所帮助。 小册子…

机器学习:信用风险评估评分卡建模方法及原理

#课程介绍 信用风险评分卡为信用风险管理提供了一种有效的、经验性的解决方法&#xff0c;是消费信贷管理中广泛应用的技术手段。 评分卡是信用风险评估领域常见的建模方法。评分卡并不加单对应于某一种机器学习算法&#xff0c;而是一种通用的建模框架&#xff0c;讲原始数据通…

0基础学怎么学习python

​ Python相对于其他编程语言来说是比较简单的&#xff0c;非常适合零基础的小白学习&#xff0c;想要进入到互联网行业&#xff0c;可以优先选择学习Python&#xff0c;那么下面小编就来为大家详细的介绍一下0基础学怎么学习python? ​  0基础学怎么学习python? 1、要读书…

nginx技术(2)nginx的配置详解

nginx的配置 1&#xff0c;启动nginx 1234567[rootcentos6 nginx-1.2.9]# /usr/sbin/nginx -c /etc/nginx/nginx.conf 启动nginx [rootcentos6 nginx-1.2.9]# ps -ef|grep nginx 查看进程 root 5479 1 0 04:15 ? 00:00:00 nginx: master process /usr/sbin/nginx -…

javascript 基础篇2 数据类型,语句,函数

文章里如果有错误的话&#xff0c;希望能帮忙指正~我也是边看视频边学习中&#xff0c;这个算是个笔记吧~自认为总结出来的东西比看视频要节省点时间~能帮到别人最好了~帮不到也起码恩能帮到我自己 嘿~ 写内容之前废话一句&#xff1a;因为旧版有些浏览器不支持javascript脚本&…

技术图文:如何在Python中定义二维数组?

背景 前几天&#xff0c;有位同学问我如下的问题&#xff1a; “temp[0][0]修改后&#xff0c;为什么temp[1][0]、temp[2][0]也发生了变化&#xff1f;” “在Python中二维数组是怎样定义和使用的&#xff1f;” 今天就来谈谈这个问题。 技术分析 在 C# 语言中有直接定义二…

javascript的垃圾回收机制指的是什么

定义&#xff1a;指一块被分配的内存既不能使用&#xff0c;又不能回收&#xff0c;直到浏览器进程结束。 像 C 这样的编程语言&#xff0c;具有低级内存管理原语&#xff0c;如 malloc()和 free()。开发人员使用这些原语显式地对操作系统的内存进行分配和释放。 而 JavaScript…

技术图文:Matlab向量 VS. Python列表

背景 前段时间在知识星球上立了一个Flag&#xff0c;至少写10篇关于 Python&#xff0c;Matlab 和 C# 对比的总结。这是第 1 篇&#xff0c;从创建结构、添加元素、删除元素、获取元素四个角度来对比 Matlab 的向量与 Python 的列表。 1. 向量/列表 的创建 1.1 直接法 Matla…

我的ExtJS学习之路 ——4

项目基本架子出来&#xff0c;然后就该考虑将封装好的gridpanel 和 tabpanel关联起来 既 点击树的叶子节点&#xff0c;将 gridpanel 展现在 tabpanel中 怎么关联呢&#xff1f; 【在之前的基础上的&#xff0c;重复的代码就不贴出来了】 我改变了 模拟数据的形式&#xff0c;注…

php CI框架输出空行问题排查

今天在使用 curl 命令行工具调试一个功能时&#xff0c;发现输出的内容总是会在最开始莫名其妙的多一行空行&#xff1a; 项目框架是 php 的 CodeIgniter&#xff0c;感觉这种问题在网上不好查找&#xff0c;因为可以确定这个是业务出现的问题&#xff0c;然后只能自己去定位查…

哪些人适合学习软件测试

软件测试相对于其他编程语言来说&#xff0c;它的入门门槛是相对比较低的&#xff0c;想要从事IT互联网行业可以选择学习软件测试&#xff0c;那么都有哪些人适合学习软件测试呢?来看看下面的详细介绍吧。 哪些人适合学习软件测试?就在软件测试培训行业观察来看&#xff0c;小…

c语言基本函数

一.内存操作函数&#xff1a; &#xff08;1&#xff09; 头文件&#xff1a;#include <string.h>memset() 函数用来将指定内存的前n个字节设置为特定的值&#xff0c;其原型为&#xff1a; void * memset( void * ptr, int value, size_t num );参数说明&#xff1a;…