精华内容
下载资源
问答
  • 通俗易懂的Kafka零拷贝机制 千次阅读 多人点赞
    2020-09-11 11:01:00

    Kafka之所以那么快,其中一个很大的原因就是零拷贝(Zero-copy)技术,零拷贝不是kafka的专利,而是操作系统的升级,又比如Netty,也用到了零拷贝。下面我就画图讲解零拷贝,如果对你有帮助请点个赞支持。

    传统IO

    kafka的数据是要落入磁盘的,那么必然牵扯到磁盘的IO,传统磁盘IO又叫缓存IO,效率是很低的,那么为什么效率低呢?我们先来粗略讲讲操作系统的知识。

    用户空间以及内核空间的概念:

    我们知道现在操作系统都是采用虚拟存储器。那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间(Kernel space),一部分为用户空间(User space)。针对Linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。

    传统的文件读写或者网络传输,通常需要将数据从内核态转换为用户态。应用程序读取用户态内存数据,写入文件 / Socket之前,需要从用户态转换为内核态之后才可以写入文件或者网卡当中。我们可以称之为read/write模式,此模式的步骤为:

    1. 首先,调用read时,磁盘文件拷贝到了内核态;
    2. 之后,CPU控制将内核态数据copy到用户态下;
    3. 调用write时,先将用户态下的内容copy到内核态下的socket的buffer中;
    4. 最后将内核态下的socket buffer的数据copy到网卡设备中传送;

     DMA

    DMA(Direct Memory Access,直接存储器访问) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载 。通俗来讲,就是DMA 传输将数据从一个地址空间复制到另外一个地址空间,当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成,也就是两个硬件之间完成的,而没有CPU的参与,那么CPU就可以释放出来做别的事情,这样极大地提高了效率。我们常见的硬件设备如网卡、磁盘设备、显卡、声卡之类的都支持DMA。

    所以上面所说的read/write模式大概如图所示:

    传统IO有两个很大的缺点导致很慢:

    1. 我们可以清楚的看到共产生了4次copy,从磁盘文件到Kernal的相互读写是支持DMA copy的,但即使是这样,从Kernal到User没有硬件的支持所以不支持DMA,还有两次CPU copy。
    2. Kafka只是把文件存放到磁盘之后通过网络发出去,中间并不需要修改什么数据,那read和write的两次CPU copy的操作完全是多余的。

     零拷贝

    mmap

    mmap是零拷贝的一种,主要就是去掉read write这两次CPU copy以提升性能,调用mmap()来代替read调用:

    buf = mmap(diskfd, len);
    write(sockfd, buf, len);

    此模式步骤为:

    1. 用户程序调用 mmap(),磁盘上的数据会通过 DMA被拷贝的内核缓冲区;
    2. 接着操作系统会把这段内核缓冲区与用户程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝;
    3. 用户程序再调用 write(),操作系统直接将内核缓冲区的内容拷贝到 socket缓冲区中;
    4. 最后, socket缓冲区再把数据发到网卡去。

    这显然是一个伟大的进步,把上下文的切换次数从4次减少到2次,同时也把数据copy的次数从4次降低到了3次。

    sendfile

    Linux2.1内核开始引入了sendfile函数,用于将文件通过socket传送。开始时跟mmap没什么区别,但是Linux2.4做出了重大优化,将零拷贝推到顶峰。

    优化后的处理过程如下:

    1. 将文件拷贝到kernel buffer中;
    2. 向socket buffer中追加当前要发生的数据在kernel buffer中的位置和偏移量;
    3. 根据socket buffer中的位置和偏移量直接将kernel buffer的数据copy到网卡设备中;

    如图:

    经过上述过程,数据只经过了2次copy就从磁盘传送出去了。这个才是真正的Zero-Copy(这里的零拷贝是针对kernel来讲的,数据在kernel模式下是Zero-Copy)。

    正是Linux2.4的内核做了改进,Java中的TransferTo()实现了Zero-Copy。

    测试

    在Windows10上测试:

    测试结果仅供参考 ,并不是平均数,所以可能偏差较大。

    更多相关内容
  • 什么是"零拷贝"技术

    千次阅读 2020-01-16 15:00:31
    下面从I/O的几个概念开始,进而在分析零拷贝。 I/O概念 1、缓冲区 缓冲区是所有I/O的基础,I/O讲的无非就是把数据移进或移出缓冲区;进程执行I/O操作,就是向操作系统发出请求,让它要么把缓冲区的数...

    前言

    从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能;这个词我们也经常在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也省掉了;

     

    mmap 和 sendFile 的区别:

    1. mmap 适合小数据量读写,sendFile 适合大文件传输。
    2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
    3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。

    在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

     

    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()方法的源码:

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

    大致意思就是通过native方法获取内存映射的地址,如果失败,手动gc再次映射;最后通过内存映射的地址实例化出MappedByteBuffer,MappedByteBuffer本身是一个抽象类,其实这里真正实例话出来的是DirectByteBuffer;

    2、DirectByteBuffer

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

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

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

    3、Channel-to-Channel传输

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

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

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

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

    几个参数也比较好理解,分别是开始传输的位置,传输的字节数,以及目标通道;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 NIO中对零拷贝的使用有哪些呢?

    NIO DirectByteBuffer

    Java NIO引入了用于通道的缓冲区的ByteBuffer。 ByteBuffer有三个主要的实现:

    HeapByteBuffer

    在调用ByteBuffer.allocate()时使用。 它被称为堆,因为它保存在JVM的堆空间中,因此您可以获得所有优势,如GC支持和缓存优化。 但是,它不是页面对齐的,这意味着如果您需要通过JNI与本地代码交谈,JVM将不得不复制到对齐的缓冲区空间。

    DirectByteBuffer

    在调用ByteBuffer.allocateDirect()时使用。 JVM将使用malloc()在堆空间之外分配内存空间。 因为它不是由JVM管理的,所以你的内存空间是页面对齐的,不受GC影响,这使得它成为处理本地代码的完美选择。 然而,你要C程序员一样,自己管理这个内存,必须自己分配和释放内存来防止内存泄漏。

    MappedByteBuffer

    在调用FileChannel.map()时使用。 与DirectByteBuffer类似,这也是JVM堆外部的情况。 它基本上作为OS mmap()系统调用的包装函数,以便代码直接操作映射的物理内存数据。

    总结

    零拷贝是操作系统底层的一种实现,我们在网络编程中,利用操作系统这一特性,可以大大提高数据传输的效率。这也是目前网络编程框架中都会采用的方式,理解好零拷贝,有助于我们进一步学习Netty等网络通信框架的底层原理。

     

     

     

     

    操作系统层面的解释:

    #零拷贝

    零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。 [1] 

    中文名:零复制

    外文名: Zero-copy

    又    称: 零拷贝

    本    质 : 一种计算机执行操作

     

    操作系统某些组件(例如驱动程序文件系统和网络协议栈)若采用零复制技术,则能极大地增强了特定应用程序的性能,并更有效地利用系统资源。通过使CPU得以完成其他而非将机器中的数据复制到另一处的任务,性能也得到了增强。另外,零复制操作减少了在用户空间与内核空间之间切换模式的次数。

    举例来说,如果要读取一个文件并通过网络发送它,传统方式下每个读/写周期都需要复制两次数据和切换两次上下文,而数据的复制都需要依靠CPU。通过零复制技术完成相同的操作,上下文切换减少到两次,并且不需要CPU复制数据。

    零复制协议对于网络链路容量接近或超过CPU处理能力的高速网络尤为重要。在这种网络下,CPU几乎将所有时间都花在复制要传送的数据上,因此将成为使通信速率低于链路容量的瓶颈。

    硬件实现

    最早的实现为IBMOS/360,其中一个程序可以指示通道子系统从一个文件或设备复制数据块到另一处,无需先转移数据。

    实现零复制的软件通常依靠基于直接存储器访问(DMA)的复制,以及通过内存管理单元(MMU)的内存映射。这些功能需要特定硬件的支持,并通常涉及到特定存储器的对齐。

    一种较新的方式为使用异构系统架构(HSA),便于CPUGPU以及其他处理器传递指针。这需要CPU和GPU使用统一地址空间。

    程序访问

    数种操作系统都通过特定API支持文件的零复制。

    Linux内核通过各个系统调用支持零复制,例如sys/socket.h的sendfile、sendfile64以及splice。它们部分在POSIX中指定,因此也存在于BSD内核或IBM AIX中,部分则是Linux内核API中独有。

    Microsoft Windows通过TransmitFile API支持零复制。

    Java输入流可以通过java.nio.channels支持零复制。FileChannel的transferTo()方法也可以支持零复制(如果底层操作系统支持)。

    远程直接内存访问(RDMA)协议深度依赖零复制技术。 [2] 

     

    #DMA

     

    DMA(直接存储器访问)

    DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

    中文名:直接存储器访问

    外文名:Direct Memory Access

    缩    写:DMA

    功    能:不同速度的硬件装置来沟通

    来    源:嵌入式底层驱动

     

    原理

    DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。像是这样的操作并没有让处理器工作拖延,反而可以被重新排程去处理其他的工作。DMA 传输对于高效能 嵌入式系统算法和网络是很重要的。

    在实现DMA传输时,是由DMA控制器直接掌管总线,因此,存在着一个总线控制权转移问题。即DMA传输前,CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU。一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束4个步骤。

    请求

    CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。

    响应

    DMA控制器对DMA请求判别优先级及屏蔽,向总线裁决逻辑提出总线请求。当CPU执行完当前总线周期即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经响应,通过DMA控制器通知I/O接口开始DMA传输。

    传输

    DMA控制器获得总线控制权后,CPU即刻挂起或只执行内部操作,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。

    在DMA控制器的控制下,在存储器和外部设备之间直接进行数据传送,在传送过程中不需要中央处理器的参与。开始时需提供要传送的数据的起始位置和数据长度。

    结束

    当完成规定的成批数据传送后,DMA控制器即释放总线控制权,并向I/O接口发出结束信号。当I/O接口收到结束信号后,一方面停 止I/O设备的工作,另一方面向CPU提出中断请求,使CPU从不介入的状态解脱,并执行一段检查本次DMA传输操作正确性的代码。最后,带着本次操作结果及状态继续执行原来的程序。

    由此可见,DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,使CPU的效率大为提高。

     

    展开全文
  • 我们也经常在 Java NIO,Netty,Kafka,RocketMQ 等框架中听到零拷贝,它经常作为其提升性能的一大亮点 下面从 I/O 的几个概念开始,进而再分析零拷贝。 I/O 概念 缓冲区 缓冲区是所有 I/O 的基础,I/O 讲的无非就是...

    公众号后台回复“面试”,获取精品学习资料

    扫描下方海报了解专栏详情

    本文来源:

    https://juejin.im/post/5cad6f1ef265da039f0ef5df

    《Java工程师面试突击(第3季)》重磅升级,由原来的70讲增至160讲,内容扩充一倍多,升级部分内容请参见文末

    零拷贝,从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能。我们也经常在 Java NIO,Netty,Kafka,RocketMQ 等框架中听到零拷贝,它经常作为其提升性能的一大亮点

    下面从 I/O 的几个概念开始,进而再分析零拷贝。

    I/O 概念

    缓冲区

    缓冲区是所有 I/O 的基础,I/O 讲的无非就是把数据移进或移出缓冲区;进程执行 I/O 操作,就是向操作系统发出请求,让它要么把缓冲区的数据排干(写),要么填充缓冲区(读)。

    下面看一个 Java 进程发起 Read 请求加载数据大致的流程图:

    进程发起 Read 请求之后,内核接收到 Read 请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据 Copy 给进程的缓冲区。

    如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核 Read 缓冲区,这一步通过 DMA 完成。

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

    你可能觉得这样挺浪费空间的,每次都需要把内核空间的数据拷贝到用户空间中,所以零拷贝的出现就是为了解决这种问题的。

    关于零拷贝提供了两种方式分别是:

    • mmap+write

    • Sendfile

    虚拟内存

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

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

    • 虚拟内存空间可大于实际可用的物理地址。

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

    大致如下图所示:

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

    mmap+write 方式

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

    这样就可以省掉原来内核 Read 缓冲区 Copy 数据到用户缓冲区,但是还是需要内核 Read 缓冲区将数据 Copy 到内核 Socket 缓冲区。

    大致如下图所示:

    Sendfile 方式

    Sendfile 系统调用在内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。

    Sendfile 系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数,大致如下图所示:

    数据传送只发生在内核空间,所以减少了一次上下文切换;但是还是存在一次 Copy,能不能把这一次 Copy 也省略掉?

    Linux2.4 内核中做了改进,将 Kernel buffer 中对应的数据描述信息(内存地址,偏移量)记录到相应的 Socket 缓冲区当中,这样连内核空间中的一次 CPU Copy 也省掉了。

    Java 零拷贝

    MappedByteBuffer

    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:映射的模式,可选项包括: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。

    DirectByteBuffer

    DirectByteBuffer 继承于 MappedByteBuffer,从名字就可以猜测出开辟了一段直接的内存,并不会占用 JVM 的内存空间。

    上一节中通过 Filechannel 映射出的 MappedByteBuffer 其实际也是 DirectByteBuffer,当然除了这种方式,也可以手动开辟一段空间:

    ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
    

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

    Channel-to-Channel 传输

    经常需要从一个位置将文件传输到另外一个位置,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 零拷贝

    Netty 提供了零拷贝的 Buffer,在传输数据时,最终处理的数据会需要对单个传输的报文,进行组合和拆分,NIO 原生的 ByteBuffer 无法做到

    Netty 通过提供的 Composite(组合)和 Slice(拆分)两种 Buffer 来实现零拷贝。

    看下面一张图会比较清晰:

    TCP 层 HTTP 报文被分成了两个 ChannelBuffer,这两个 Buffer 对我们上层的逻辑(HTTP 处理)是没有意义的。

    但是两个 ChannelBuffer 被组合起来,就成为了一个有意义的 HTTP 报文,这个报文对应的 ChannelBuffer,才是能称之为“Message”的东西,这里用到了一个词“Virtual Buffer”。

    可以看一下 Netty 提供的 CompositeChannelBuffer 源码:

    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 里面对象的概率来理解的话,其实就是使用的都是对象的引用,每个引用对象的地方对其改变就都能改变此对象,永远只存在一份对象。

    END

    《Java工程师面试突击第三季》加餐部分大纲:(注:1-66讲的大纲请扫描文末二维码,在课程详情页获取)

    详细的课程内容,大家可以扫描下方二维码了解:

    展开全文
  • 深入浅出操作系统的零拷贝

    千次阅读 2022-02-07 00:21:46
    在 kafka、netty 这些技术中,零拷贝都是一个重要的考点。但是零拷贝与这些具体的技术无关,关键点是数据传输。就像冰糖葫芦里的山楂:冰糖葫芦里重要的组成可以有山楂。但是山楂并不是冰糖...

    在 kafka、netty 这些技术中,零拷贝都是一个重要的考点。但是零拷贝与这些具体的技术无关,关键点是数据传输。就像冰糖葫芦里的山楂:冰糖葫芦里重要的组成可以有山楂。但是山楂并不是冰糖葫芦特有的,羊羹里也可以有。

    51cf373dd3156e387af376018c014a10.png

    下面是一个 MQ 的基本流程。

    5c48764e70801f4c28081e37b19253b9.png

    如果采用传统方法进行数据传输,消息从存储系统到达消费者需要经过4次拷贝。如果使用零拷贝技术,情况会怎样呢?

    传统模式下的数据拷贝过程

    过程解释

    传统模式下,上图红框中经过了从文件读数据和从 socket 进行数据发送两个过程。

    46b20a338f88ddae250d6180ec42ca0b.png

    内部流程如下图所示:

    31b753b25737fd520d6092bf8871b7f6.png

    用户进程如 Java 程序想进行 File.read ,需要将数据进行 DMA 拷贝读取到到文件读取缓冲区。还记得《时刻掌握系统运行状态-深度理解top命令》里的 buffers/cached 空间吗?文件读取缓冲区占用的就是这个空间。

    文件读取缓冲区仍然是内核空间,用户进程要使用还需要进行一次 CPU 拷贝将数据拷贝到应用进程缓冲区。这时候用户进程比如 Java 程序就可以对数据进行排序、过滤等操作了。这个过程就完成了 File.read。

    如果数据想发送到网卡,也就是 Socket.send。还需要再进行一次 CPU 拷贝发送到套接字发送缓冲区进行中转,这个地方也是要占用 buffers/cached 空间的。中转这个过程很快,所以 buffers/cached 空间可以很快被释放。

    数据从中转站还要进行一次 DMA 拷贝,将数据运送到网络设备缓冲区,最终发送到网络上。这个过程就完成了 Socket.send。

    这个过程要进行几次上下文切换呢?File.read 这个函数需要先调用发起内核请求,进入到内核空间操作,这是一次内核切换。内核操作完成返回内核的结果,这是第二次内核切换。同理, Socket.send 也需要两次内核切换。这里的用户态到内核态的切换就是上下文切换。总共是4次。

    性能测试

    这种方式性能如何呢?咱们来测试一下。

    写个程序从本地电脑中读取自己的一张照片,这张照片5M多大,发送到服务端。

    2d6b0bb00d8a96d68e528eae716d17d0.png

    服务端只要能接收客户端数据就可以,我随便写了一个:

    public static void server() throws Exception {
            ServerSocket serverSocket = new ServerSocket(520);
            int i = 1;
            while (true) {
                Socket socket = serverSocket.accept();
                int left = 0;
                while (left >= 0) {
                    InputStream io = socket.getInputStream();
                    byte[] bytes = new byte[1024];
                    left = io.read(bytes);
                }
            }
        }

    客户端读取数据并发送到网络:

    @GetMapping(path = "hi")
    public String hi() throws Exception {
        client();
        return "end";
    }
    
    
    public void client() throws Exception {
        Socket socket = new Socket("127.0.0.1", 520);
        //向服务器端第一次发送字符串
        OutputStream netOut = socket.getOutputStream();
        InputStream io = new FileInputStream("D:\\photo\\编程一生.JPG");    long begin = System.currentTimeMillis();
        byte[] bytes = new byte[1024];
        while (io.read(bytes) >= 0) {
            netOut.write(bytes);
        }
        System.out.println("耗时为" + (System.currentTimeMillis() - begin) + "ms");
        netOut.close();
        io.close();
        socket.close();
    }

    服务启动后:http://localhost:8080/hi 访问5次,结果如下:

    耗时为450ms

    耗时为437ms

    耗时为424ms

    耗时为423ms

    耗时为420ms

    结论:使用传统方式,5M多的数据读取到发送需要400多毫秒。

    零拷贝过程

    过程解释

    linux操作系统中有个 sendFile 方法可以不通过用户进行,直接将数据从磁盘发送到网络设备缓冲区。在 linux2.1 版本的 sendFile 过程如下图:

    fa0c01b1c5e7973443be1b7678816e20.png

    到了 linux2.4 ,linux 的 sendFile 进行了优化,实现了完全没有 CPU 拷贝实现数据传输。

    0ea0a5635e375b9b72bf538ae36634b5.png

    不管是 linux2.1 还是 linux2.4 ,都是 linux 自身实现的,函数都对应的是 sendFile 。上层比如 Java 可以使用 transferTo 和 transferFrom 使用 sendFile 方法,这两个方法是 netty 实现的重要工具,一个是发送数据用,一个是接收数据用。

    性能测试

    这种方式性能如何呢?咱们来测试一下。

    服务端不变,客户端代码如下:

    @GetMapping(path = "hi")
    public String hi() throws Exception {
        client();
        return "end";
    }
    
    
     public void client() throws Exception {
          SocketChannel socket = SocketChannel.open();
          socket.connect(new InetSocketAddress("127.0.0.1", 520));
          FileChannel io = new FileInputStream("D:\\photo\\编程一生.JPG").getChannel();
          long begin = System.currentTimeMillis();
          io.transferTo(0, io.size(), socket);
          System.out.println("耗时为" + (System.currentTimeMillis() - begin) + "ms");
          io.close();
          socket.close();
      }

    服务启动后:http://localhost:8080/hi 访问5次,结果如下:

    耗时为44ms

    耗时为33ms

    耗时为43ms

    耗时为46ms

    耗时为35ms

    结论:使用零拷贝方式,5M多的数据读取到发送需要40多毫秒。与传统方式相比,性能提高10倍。

    内存映射模式与零拷贝

    linux 系统有零拷贝,windows 也希望减少拷贝和下上下切换,它依靠内存映射(MMAP)。当然,linux 也支持内存映射,并且在 RocketMQ 等的实现上发挥着巨大作用。

    7ca2fd72c985710581da117e0f93883c.png

    通过与上面传统方式比较,可看到由于内存映射发挥作用,在文件读取时减少了一次 CPU 拷贝。

    在 Java 中可以通过下面方法进行内存映射:

    RandomAccessFile raf = new RandomAccessFile(file, "rw");
     MappedByteBuffer mmap = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, 500);

    在 MQ 的实现上,内存映射(MMAP)和 sendFile 零拷贝是提升性能的利器。下面做一个比较:

    31ce6d9d238719c58ad43645d9a63f2e.png

    上面可以看到 RocketMQ 由于使用了内存映射吞吐量远高于 ActiveMQ 和 RabbitMQ ,Kafka 由于使用了零拷贝又比 RocketMQ 提高了一个数量级。

    实际上 RabbitMQ 的实现大量借鉴了 Kafka ,那 RabbitMQ 为什么不直接使用 Kafka 的零拷贝提高性能呢?因为 RabbitMQ 不仅仅是将数据从磁盘发送出去,还需要在内存中做一些排序、过滤等高级操作。

    最后大家再来思考一个问题:零拷贝和内存映射两种模式下,各需要几次上下文切换?

    展开全文
  • 零拷贝(zero-copy)

    千次阅读 2022-03-21 14:13:57
    零拷贝
  • 《Linux Zero-copy零拷贝技术全面揭秘》 《什么是mmap?零拷贝?DMA?》 《Linux C语言:用零拷贝技术实现TCP代理(源代码+测试服务端客户端代码)》 《Kafka和RocketMQ底层存储:零拷贝技术》 《Linux I/O原理...
  • JAVA的几种实现零拷贝方式

    千次阅读 2020-07-17 14:02:52
    下面从I/O的几个概念开始,进而在分析零拷贝。 I/O概念 1.缓冲区 缓冲区是所有I/O的基础,I/O讲的无非就是把数据移进或移出缓冲区;进程执行I/O操作,就是向操作系统发出请求,让它要么把缓冲区的数据排干(写),...
  • 异步IO、epoll、零拷贝

    2021-07-21 10:52:15
    异步 IO 模型(AIO)是真正意义上的无阻塞的IO操作,但是目前只有windows支持AIO,linux内核暂时不支持。 所以,理论上liinux的epoll尽管很猛,但是read的第二阶段:内核空间到用户空间还是阻塞的。 参考:WX收藏夹...
  • 零拷贝,从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能。我们也经常在 Java NIO,Netty,Kafka,RocketMQ 等框架中听到零拷贝,它经常作为其提升性能的一大亮点;下面从 I/O 的几个概念开始,进而...
  • 零拷贝

    千次阅读 2019-06-08 23:15:03
    本文就介绍一下其中一个技术——零拷贝。 到目前为止,几乎每个人都听过 Linux 中所谓的零拷贝功能,但是我经常遇到对它不完全理解的人。基于此,我决定写一些文章深入探讨这个有用的功能。在这篇文章中,我们将从...
  • 零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间。图片来...
  • Linux 零拷贝技术

    千次阅读 2017-03-25 16:28:20
    零拷贝(zero-copy)技术可以减少数据拷贝和共享总线操作的次数,消除...采用零拷贝技术,通过减少数据拷贝次数,简化协议处理的层次,在应用和网络间提供更快的数据通路,可以有效地降低通信延迟,增加网络吞吐率。
  • 零拷贝技术

    千次阅读 2014-05-05 20:41:13
    如今几乎每个人都听说过Linux中所谓的"零拷贝"特性,然而我经常碰到没有充分理解这个问题的人们。因此,我决定写一些文章略微深入的讲述这个问题,希望能将这个有用的特性解释清楚。在本文中,将从用户空间应用程序...
  • 点击上方“朱小厮的博客”,选择“设为星标”后台回复”加群“获取公众号专属群聊入口来源:rrd.me/ggFBd零拷贝,从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能。我们也...
  • 目录 导言 计算机存储器 物理内存 虚拟内存 静态重定位 ...零拷贝 (Zero-copy) Zero-copy 是什么? Zero-copy 能做什么? Zero-copy 的实现方式有哪些? mmap() sendfile() sendfile() with DMA S
  • Java中的零拷贝

    2022-04-09 13:41:02
    先提出两个问题: IO过程中,哪些步骤进行了拷贝?哪些地方零拷贝?...零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络.
  • 文章目录DMA系统调用内核缓冲区读写:虚拟内存:零拷贝:传统IO①mmap零拷贝②sendfile零拷贝零拷贝总结mmap 和 sendFile 的区别NIO的buffer:NIO必然使用了堆外空间:java-NIO 零拷贝案例 DMA DMA: direct memory ...
  • JavaNIO和零拷贝(Zero Copy)

    万次阅读 多人点赞 2018-05-10 22:32:34
    一.Linux操作系统中的零拷贝 1.1先从Linux的普通I/O过程说起 1.2内存映射方式I/O 1.3内核空间内部传输I/O 1.4理想状态下的零拷贝I/O 1.5splice()系统调用 二.JavaNIO中的零拷贝 2.1NIO中内存映射方式I/O 2.2NIO中...
  • 使用零拷贝请求的应用程序,内核将数据直接从磁盘文件复制到套接字,而不通过应用程序。 零拷贝大大提高了应用程序的性能,并减少了内核和用户模式之间的上下文切换次数。 Java类库通过 transferTo() in方法在  ...
  • 程序员的成长之路互联网/程序员/技术/资料共享关注阅读本文大概需要 7分钟。来源:ksfzhaohui juejin.im/post/5cad6f1ef265da039f0ef5df...
  • 前言 零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 ...实现零拷贝用到的最主要技术是 DMA 数据传输技术和内存区域映射技术。 ...
  • NIO、BIO编程模型与零拷贝

    千次阅读 2020-02-21 11:19:53
    Java IO模型 Java共支持3种网络编程... //在linux下一个transferTo 方法就可以完成传输 //在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件 //transferTo 底层使用到零拷贝 long transferCount = ...
  • IO零拷贝_Linux

    2018-03-15 10:05:31
    目录目录 简介 抛砖引玉 mmap sendfile 支持聚集操作的硬件 在Java中的应用 参考 简介零拷贝(zero-copy)技术可以减少数据拷贝和共享总线操作的次数,消除通信...采用零拷贝技术,通过减少数据拷贝次数,简化协议处
  • HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级 案例说明 NIO 的 Buffer public class BasicBuffer { public static void main(String[] args) { ...
  • 从这篇文章开始,作者将带着大家来学习《Windows黑客编程技术详解》,其作者是甘迪文老师,推荐大家购买来学习。作者将采用实际编程和图文结合的方式进行分享,并且会进一步补充知识点。第一篇文章主要包括两部分...
  • 零拷贝IO6. BIO7. NIO8. IO多路复用8.1 select8.2 poll8.3 epoll9. AIO10. 总结 1. 什么是I/O 在计算机操作系统中,所谓的I/O就是输入(Input)和输出(Output),也可以理解为读(Read)和写(Write),针对不同的...
  • 实现零拷贝用到的最主要技术是 DMA 数据传输技术和内存区域映射技术零拷贝机制可以减少数据在内核缓冲区和用户进程缓冲区之间反复的 I/O 拷贝操作。 零拷贝机制可以减少用户进程地址空间和内核地址
  • windows搭建googletest测试工程

    千次阅读 2021-11-07 22:42:02
    本文介绍使用googletest在windows下从搭建自己的单元测试工程。 0. 环境准备 系统:Windows10 vscode: https://code.visualstudio.com/ mingw-64:https://www.mingw-w64.org/downloads/ cmake: ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,863
精华内容 7,545
关键字:

windows零拷贝技术