百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

面试被问到“零拷贝”!你真的理解吗?

ztj100 2024-11-09 15:19 12 浏览 0 评论

本文来源:http://suo.im/679vJd

  • 前言
  • I/O概念
    • 1.缓冲区
    • 2.虚拟内存
    • 3.mmap+write方式
    • 4.sendfile方式
  • Java零拷贝
    • 1.MappedByteBuffer
    • 2.DirectByteBuffer
    • 3.Channel-to-Channel传输
  • Netty零拷贝
  • 其他零拷贝
  • 总结
  • 前言

    从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能;这个词我们也经常在java nio,netty,kafka,RocketMQ等框架中听到,经常作为其提升性能的一大亮点;下面从I/O的几个概念开始,进而在分析零拷贝。

    I/O概念

    1.缓冲区

    缓冲区是所有I/O的基础,I/O讲的无非就是把数据移进或移出缓冲区;进程执行I/O操作,就是向操作系统发出请求,让它要么把缓冲区的数据排干(写),要么填充缓冲区(读);下面看一个java进程发起read请求加载数据大致的流程图:

    进程发起read请求之后,内核接收到read请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据copy给进程的缓冲区;如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核read缓冲区,这一步通过DMA完成;

    接下来就是内核将数据copy到进程的缓冲区;如果进程发起write请求,同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面,然后再通过DMA把数据copy到网卡中,发送出去;

    你可能觉得这样挺浪费空间的,每次都需要把内核空间的数据拷贝到用户空间中,所以零拷贝的出现就是为了解决这种问题的;关于零拷贝提供了两种方式分别是:mmap+write方式,sendfile方式;

    2.虚拟内存

    所有现代操作系统都使用虚拟内存,使用虚拟的地址取代物理地址,这样做的好处是:

    1.一个以上的虚拟地址可以指向同一个物理内存地址

    2.虚拟内存空间可大于实际可用的物理地址;

    利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样DMA就可以填充对内核和用户空间进程同时可见的缓冲区了,大致如下图所示:

    省去了内核与用户空间的往来拷贝,java也利用操作系统的此特性来提升性能,下面重点看看java对零拷贝都有哪些支持。

    3.mmap+write方式

    使用mmap+write方式代替原来的read+write方式,mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系

    这样就可以省掉原来内核read缓冲区copy数据到用户缓冲区,但是还是需要内核read缓冲区将数据copy到内核socket缓冲区,大致如下图所示:

    4.sendfile方式

    sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数,大致如下图所示:

    数据传送只发生在内核空间,所以减少了一次上下文切换;但是还是存在一次copy,能不能把这一次copy也省略掉,Linux2.4内核中做了改进,将Kernel buffer中对应的数据描述信息(内存地址,偏移量)记录到相应的socket缓冲区当中,这样连内核空间中的一次cpu copy也省掉了;

    Java零拷贝

    1.MappedByteBuffer

    java nio提供的FileChannel提供了map()方法,该方法可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射,MappedByteBuffer继承于ByteBuffer,类似于一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘的一个文件中

    调用get()方法会从磁盘中获取数据,此数据反映该文件当前的内容,调用put()方法会更新磁盘上的文件,并且对文件做的修改对其他阅读者也是可见的;下面看一个简单的读取实例,然后在对MappedByteBuffer进行分析:

    主要通过FileChannel提供的map()来实现映射,map()方法如下:

    分别提供了三个参数,MapMode,Position和size;分别表示:MapMode:映射的模式,可选项包括:READ_ONLY,READ_WRITE,PRIVATE;Position:从哪个位置开始映射,字节数的位置;Size:从position开始向后多少个字节;

    重点看一下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;

    2.DirectByteBuffer

    DirectByteBuffer继承于MappedByteBuffer,从名字就可以猜测出开辟了一段直接的内存,并不会占用jvm的内存空间;上一节中通过Filechannel映射出的MappedByteBuffer其实际也是DirectByteBuffer,当然除了这种方式,也可以手动开辟一段空间:

    ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);

    如上开辟了100字节的直接内存空间;

    3.Channel-to-Channel传输

    经常需要从一个位置将文件传输到另外一个位置,FileChannel提供了transferTo()方法用来提高传输的效率,首先看一个简单的实例:

    通过FileChannel的transferTo()方法将文件数据传输到System.out通道,接口定义如下:

        public abstract long transferTo(long position, long count,
                                        WritableByteChannel target)
            throws IOException;
    

    几个参数也比较好理解,分别是开始传输的位置,传输的字节数,以及目标通道;transferTo()允许将一个通道交叉连接到另一个通道,而不需要一个中间缓冲区来传递数据;注:这里不需要中间缓冲区有两层意思:第一层不需要用户空间缓冲区来拷贝内核缓冲区,另外一层两个通道都有自己的内核缓冲区,两个内核缓冲区也可以做到无需拷贝数据;

    Netty零拷贝

    netty提供了零拷贝的buffer,在传输数据时,最终处理的数据会需要对单个传输的报文,进行组合和拆分,Nio原生的ByteBuffer无法做到,netty通过提供的Composite(组合)和Slice(拆分)两种buffer来实现零拷贝;看下面一张图会比较清晰:

    TCP层HTTP报文被分成了两个ChannelBuffer,这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。但是两个ChannelBuffer被组合起来,就成为了一个有意义的HTTP报文,这个报文对应的ChannelBuffer,才是能称之为”Message”的东西,这里用到了一个词”Virtual Buffer”。可以看一下netty提供的CompositeChannelBuffer源码:

    components用来保存的就是所有接收到的buffer,indices记录每个buffer的起始位置,lastAccessedComponentId记录上一次访问的ComponentId;CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写,实现了零拷贝。

    其他零拷贝

    RocketMQ的消息采用顺序写到commitlog文件,然后利用consume queue文件作为索引;RocketMQ采用零拷贝mmap+write的方式来回应Consumer的请求;同样kafka中存在大量的网络数据持久化到磁盘和磁盘文件通过网络发送的过程,kafka使用了sendfile零拷贝方式;

    总结

    零拷贝如果简单用java里面对象的概率来理解的话,其实就是使用的都是对象的引用,每个引用对象的地方对其改变就都能改变此对象,永远只存在一份对象


    对了,在这里说一下,我目前是在职Java开发,如果你现在正在学习Java,了解Java,渴望成为一名合格的Java开发工程师,在入门学习Java的过程当中缺乏基础入门的视频教程,可以关注并私信我:01。获取。我这里有最新的Java基础全套视频教程。

    相关推荐

    Vue 技术栈(全家桶)(vue technology)

    Vue技术栈(全家桶)尚硅谷前端研究院第1章:Vue核心Vue简介官网英文官网:https://vuejs.org/中文官网:https://cn.vuejs.org/...

    vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)

    前言《vue基础》系列是再次回炉vue记的笔记,除了官网那部分知识点外,还会加入自己的一些理解。(里面会有部分和官网相同的文案,有经验的同学择感兴趣的阅读)在开发时,是不是遇到过这样的场景,响应...

    vue3 组件初始化流程(vue组件初始化顺序)

    学习完成响应式系统后,咋们来看看vue3组件的初始化流程既然是看vue组件的初始化流程,咋们先来创建基本的代码,跑跑流程(在app.vue中写入以下内容,来跑流程)...

    vue3优雅的设置element-plus的table自动滚动到底部

    场景我是需要在table最后添加一行数据,然后把滚动条滚动到最后。查网上的解决方案都是读取html结构,暴力的去获取,虽能解决问题,但是不喜欢这种打补丁的解决方案,我想着官方应该有相关的定义,于是就去...

    Vue3为什么推荐使用ref而不是reactive

    为什么推荐使用ref而不是reactivereactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代optionapi的data的替...

    9、echarts 在 vue 中怎么引用?(必会)

    首先我们初始化一个vue项目,执行vueinitwebpackechart,接着我们进入初始化的项目下。安装echarts,npminstallecharts-S//或...

    无所不能,将 Vue 渲染到嵌入式液晶屏

    该文章转载自公众号@前端时刻,https://mp.weixin.qq.com/s/WDHW36zhfNFVFVv4jO2vrA前言...

    vue-element-admin 增删改查(五)(vue-element-admin怎么用)

    此篇幅比较长,涉及到的小知识点也比较多,一定要耐心看完,记住学东西没有耐心可不行!!!一、添加和修改注:添加和编辑用到了同一个组件,也就是此篇文章你能学会如何封装组件及引用组件;第二能学会async和...

    最全的 Vue 面试题+详解答案(vue面试题知识点大全)

    前言本文整理了...

    基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时

    今天给大家分享的是Vue3聊天实例中的朋友圈的实现及登录验证和倒计时操作。先上效果图这个是最新开发的vue3.x网页端聊天项目中的朋友圈模块。用到了ElementPlus...

    不来看看这些 VUE 的生命周期钩子函数?| 原力计划

    作者|huangfuyk责编|王晓曼出品|CSDN博客VUE的生命周期钩子函数:就是指在一个组件从创建到销毁的过程自动执行的函数,包含组件的变化。可以分为:创建、挂载、更新、销毁四个模块...

    Vue3.5正式上线,父传子props用法更丝滑简洁

    前言Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性...

    Vue 3 生命周期完整指南(vue生命周期及使用)

    Vue2和Vue3中的生命周期钩子的工作方式非常相似,我们仍然可以访问相同的钩子,也希望将它们能用于相同的场景。...

    救命!这 10 个 Vue3 技巧藏太深了!性能翻倍 + 摸鱼神器全揭秘

    前端打工人集合!是不是经常遇到这些崩溃瞬间:Vue3项目越写越卡,组件通信像走迷宫,复杂逻辑写得脑壳疼?别慌!作为在一线摸爬滚打多年的老前端,今天直接甩出10个超实用的Vue3实战技巧,手把...

    怎么在 vue 中使用 form 清除校验状态?

    在Vue中使用表单验证时,经常需要清除表单的校验状态。下面我将介绍一些方法来清除表单的校验状态。1.使用this.$refs...

    取消回复欢迎 发表评论: