Netty是Java语言中一个高性能的网络通信框架,零拷贝又是这个框架的特色之一,它是如何实现的呢?
在计算机中完成一次数据传输,一般需要经过两个阶段。第一步, *** 作系统把数据从本地硬盘或网卡拷贝到内核空间的内存;第二步,应用程序再把数据从系统内核空间的内存拷贝到用户空间的内存;接下来才是应用程序中的数据处理工作。
先来看几个名词。
DMA(Direct Memory Access)直接存储器访问,将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个传输动作后,传输动作本身是由DMA控制器(DMAC)来完成的。也就是说在数据传输期间,系统可以并行执行其他任务。CPU拷贝,是由CPU直接处理的数据的传送,数据拷贝时一直占用CPU资源。
从上图中可以看出,传统的IO读写流程,包括4次用户态和内核态的切换,4次上下文切换,4次的数据拷贝,2次CPU拷贝,2次DMA拷贝。
一、什么是零拷贝?
拷贝,是指数据从一个存储区域复制到另一个存储区域。 零,表示次数为0,复制的次数为0,也就是数据不需要从一个存储区域复制到另一个存储区域。
二、为什么需要零拷贝?
零拷贝,就是指从系统内核空间的内存到用户空间的内存,不需要采用传统方式的数据复制。而是将系统内核空间的内存和用户空间的内存实现关联映射(mmap内存映射机制),从而省去了数据传输过程中的复制。
mmap(memory map)内存映射机制,简单来说就是将文件/设备映射到内存中,进程可以通过读写内存的方式,实现对mmap文件的 *** 作。零拷贝并不是完全没有拷贝,而是减少了数据拷贝的次数。
三、零拷贝在Netty中的三种实现。
1.使用堆外内存,也叫直接内存(Direct Memory)。netty的接收和发生都是使用Direct buffer,对应系统底层的mmap机制,直接使用堆外内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
2.提供了组合buffer对象 (CompositeByteBuf),可以聚合多个ByteBuffer对象,用户只需要像 *** 作一个ByteBuffer一样 *** 作组合ByteBuffer,避免了传统通过内存拷贝的方式将几个buffer合并成一个大buffer,不需要内存拷贝。
3.文件传输采用TransferTo方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题。
最后总结
通过整理可以发现,netty的零拷贝并不是完全不拷贝,而是减少了CPU拷贝,也就是数据从系统内核空间的内存到用户空间内存的拷贝。DMA拷贝还是存在的,毕竟它是 *** 作系统所做的事情,不属于应用程序的 *** 作范围。在netty中,目前有三种方式实现的零拷贝。第一种使用堆外内存。第二种,CompositeByteBuf组合buffer对象。第三种,文件传输采用TransferTo方法。
参考文档: https://mp.weixin.qq.com/s/HvdiDbkMMMcGhee5Dhq_Jw
netty的零拷贝技术主要基于以下几点:
1. 堆外内存,也叫直接内存
2. Composite Buffers
3. 文件传输基于linux的sendfile机制
Linux的设计的初衷:给不同的 *** 作给与不同的“权限”。Linux *** 作系统就将权限等级分为了2个等级,分别就是 内核态和用户态。
内核态是属于cpu的特权工作模式,可以 *** 作计算机设备中的任何元件,包括网卡、硬盘、内存等等。
用户态是应用程序的工作模式,只能 *** 作已申请的内存空间,无法 *** 作外围设备。当应用程序需要与网卡、硬盘等外围设备进行交互时,需要通过系统提供的接口,来调用外围设备。
堆内存中的数据如果需要发送到外围设备,需要调用系统的接口,将数据拷贝到堆外内存中,发送到外围设备中。
而Netty的ByteBuffer不经过堆内存,直接在堆外内存中进行读写,省去一步拷贝 *** 作。
需要注意的是,堆外内存只能通过主动调用回收或者Full GC回收,如果使用不当,容易造成内存溢出。
Composite Buffers
Netty提供了Composite Buffers来组合多个buffer。传统的buffer如果要合并的话,需要新建一个buffer,将原来的buffer拷贝到新的buffer中进行合并。而Composite Buffers相当于buffer的集合,保存了每个buffer对象,使物理的buffer合并变为逻辑上的buffer的合并。
文件传输
Netty的文件传输是依赖于 *** 作系统的零拷贝技术。
一般我们读取文件都是调用 *** 作系统接口, *** 作系统在应用程序读取文件时,会首先判断文件是否在内核缓冲区中,如果不在,则需要将文件从磁盘或socket读取到内核缓冲区中。
在写入文件时, *** 作系统会将文件先写入内核缓冲区,再写入到socket中。
我们传统做文件拷贝或传输时,会先在应用程序内存中构建一个缓冲区,通过这个缓冲区与 *** 作系统做数据交换。这样无疑会增加了文件的多次拷贝。
传统的文件传输过程如下:
1. 构建byte[]数组来缓冲文件
2. 切换到内核态,将文件先在内核缓冲区中缓存
3. 将内核缓冲区的数据拷贝到应用程序缓冲区的byte[]数组中
4. 切换回用户态
5. 执行写入 *** 作,切换回内核态
6. 将数据再拷贝一份到内核中的socket缓冲区
7. 切换回用户态
8. *** 作系统将数据异步刷新到网卡
传统的文件传输过程,会造成 *** 作系统在用户态和内核态多次切换,非常影响性能。
而linux在内核2.1中引入了sendfile *** 作,过程如下:
1. 读取数据时,sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中
2. 写入数据时,数据直接复制到socket关联的缓冲区(linux内核2.4已删除这一步,取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎)
3. 最后将socket buffer中的数据copy到网卡设备中(protocol buffer)发送
netty的FileRegion 包下的FileChannel.tranferTo即是基于sendfile机制来实现文件传输的
参考文章: 浅析Linux中的零拷贝技术
内核和用户空间,共享内存。数据copy到内核区后,只需要把地址共享给应用程序即可,无需再copy一次数据到用户空间。
优点:
缺点:
应用:
kafka生产者发送消息到broker的时候,broker的网络接收到数据后,copy到broker的内核空间。然后通过mmap技术,broker会修改消息头,添加一些元数据。所以,写入数据很快。当然顺序IO也是关键技术
内核直接发送数据到socket,无需用户空间参与。
优点:
缺点:
为了节省内核里面的一次copy,我们可以使用优化过的sendfile。该系统方法需要由特定的硬件来支持,并不是所有系统都支持。如下:
sendfile的时候,直接把内核空间的地址传递给socket缓存,DMA直接从指定地址读取数据到流里面。
sendfile只适用于将数据从文件拷贝到套接字上,限定了它的使用范围。Linux在2.6.17版本引入splice系统调用,用于在两个文件描述符中移动数据。
splice调用在两个文件描述符之间移动数据,而不需要数据在内核空间和用户空间来回拷贝。他从fd_in拷贝len长度的数据到fd_out,但是有一方必须是管道设备,这也是目前splice的一些局限性。flags参数有以下几种取值:
splice调用利用了Linux提出的管道缓冲区机制, 所以至少一个描述符要为管道。
以上几种零拷贝技术都是减少数据在用户空间和内核空间拷贝技术实现的,但是有些时候,数据必须在用户空间和内核空间之间拷贝。这时候,我们只能针对数据在用户空间和内核空间拷贝的时机上下功夫了。Linux通常利用写时复制(copy on write)来减少系统开销,这个技术又时常称作COW。
摘录网上:
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下—举例来说,fork()后立即调用exec()—它们就无需复制了。fork()的实际开销就是复制父进程的页表以及给子进程创建惟一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。这里补充一点:Linux COW与exec没有必然联系。
我总结下: copy-on-write技术其实是一种延迟复制的技术,只有需要用(写)的时候,才去复制数据。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)