Netty中零拷贝的理解

首先,我想介绍一下传统上零拷贝的含义,事实上,Netty中的零拷贝又有所区别,最后再介绍Netty中零拷贝的四种实现方式。

数据传输的相关概念

      计算机早期的数据传输方式:

Netty中零拷贝的理解_第1张图片

整个流程:

  1. CPU主动启动I/O设备;
  2. CPU不断询问I/O设备是否准备好;
  3. 若I/O设备准备好了,CPU开始从I/O设备中读数据;
  4. CPU将数据传输给主存。

从整个数据传输流程可以看出:CPU是一直被占用的,无法处理其他任务,性能比较差。

接口模块

       早期阶段的结构中,每个部件都有单独连线,线多而且不容易扩展,称为分散连接;之后,引入了总线连接,将I/O设备连接到一条总线上,形成了公共传输通道。

Netty中零拷贝的理解_第2张图片

这种模式下,数据传输采用了一种程序中断的方式,相当于将早期CPU不断询问I/O设备的过程去掉,流程变为:

  1. CPU主动启动I/O设备;
  2. CPU启动I/O设备后,不再询问,做其他事;
  3. 当I/O设备准备好了,则CPU开始从I/O设备读取数据;
  4. CPU将数据传输给主存。

DMA(Direct Memory Access)直接内存存取

     上述方式在一定程度上提高了CPU的利用率,但在程序中断时,CPU仍然被占用,为此,引入了DMA原理,主存和I/O设备之间有一条数据通路,如此一来,主存和I/O设备之间传输数据就不用中断CPU了。

Netty中零拷贝的理解_第3张图片

传统的零拷贝

Netty中零拷贝的理解_第4张图片

传统的发送数据要经过四个阶段,两次DMA。两次CPU中断,,共有四次拷贝,四次上下文切换,占用了两次CPU:

  1. CPU发送指令给I/O设备的DMA,由DMA将磁盘中的数据传输到内核空间的kernal biffer上;
  2. 之后触发CPU中断,CPU开始将内核空间的数据拷贝到用户空间的应用缓存上;
  3. CPU将数据从应用缓存拷贝到内核空间的socket buffer上;
  4. DMA将socket buffer上的数据拷贝到网卡缓存上。

从整个流程可以看出,传统传输数据的优缺点:

优点:开发成本低;

缺点:多次上下文切换,多次占用CPU资源,性能不好。

零拷贝

传统意义上的零拷贝相当于去掉上述流程的第2和3个阶段,计算机在网络上发送文件时,不需要将数据从内核空间拷贝到用户空间,而是由内核空间直接传输到网络上。Java NIO中文件通道的transferTo()方法实现了OS的sendfile,利用channel完成了零拷贝。

Netty中零拷贝的理解_第5张图片

整个传输流程如下:

  1. 调用sendfile(),CPU发指令让DMA 将磁盘数据拷贝到内核缓存(kernal buffer)中;
  2. DMA拷贝完成发出中断请求,进行CPU拷贝,拷贝到socket buffer中;
  3. sendfile()调用完成返回,DMA将socket buffer中的数据拷贝到网卡缓存中。 

         整个流程并未将数据从内核空间拷贝到用户空间,即为零拷贝。虽然内存拷贝减少到3次,但仍要中断CPU来复制数据,原因在于DMA需要知道内存地址才能发送数据。在Linux2.4内核中进行了改进,将内核缓存(kernel buffer)中对应的数据描述信息(偏移量等)记录到相应的socket buffer中,形成了以下传输过程: 

Netty中零拷贝的理解_第6张图片

  由于上述过程不需要CPU参与拷贝过程,效率是最好的,而Netty就是采用这种方式。

 Netty的零拷贝

   Netty的零拷贝与上述意义上的零拷贝不太一样,更多的是用户态的(Java层面),其零拷贝更多的是偏向优化数据操作的层面。

Netty的零拷贝主要体现在:

  • Netty提供了CompositeByteBuf(复合缓冲区),可以将多个ByteBuf合并成一个逻辑上的ByteBuf,避免各个ByteBuf间的拷贝;
  • 通过wrap操作,可以将byte[]数组,ByteBuf,ByteBuffer等包装成一个Netty ByteBuf对象,进而避免拷贝操作;
  • ByteBuf支持slice(分区)操作,可以将ByteBuf分解成多个共享同一个存储区域的ByteBuf,避免内存的拷贝;
  • 通过FileRegion包装的FileChannel的transferTo实现文件传输,直接将文件缓冲区的数据发送到目标channel,避免传统通过循环write方式导致的内存拷贝问题。

1、CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。

使用方式:

CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); 
compositeByteBuf.addComponents(true, ByteBuf1, ByteBuf1); 

注意: addComponents第一个参数必须为true,那么writeIndex才不为0,才能从compositeByteBuf中读到数据。

2、wrapedBuffer()方法,将byte[]数组包装成ByteBuf对象。

byte[] bytes = data.getBytes();ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes); 

Unpooled.wrappedBuffer(bytes)就是进行了byte[]数组的包装工作,过程中不存在内存拷贝。

即包装出来的ByteBuf和byte[]数组指向了同一个存储空间。因为值引用,所以bytes修改也会影响 byteBuf 的值。

3、ByteBuf的分割,slice()方法。将一个ByteBuf对象切分成多个ByteBuf对象。

ByteBuf directByteBuf = ByteBufAllocator.DEFAULT.directBuffer(1024);ByteBuf header = directByteBuf.slice(0,50);ByteBuf body = directByteBuf.slice(51,1024); 

header和body两个ByteBuf对象实际上还是指向directByteBuf的存储空间。

具体的四种实现Netty的零拷贝的实现请参照:https://segmentfault.com/a/1190000007560884

参考博文:

https://www.jianshu.com/p/82d7269521a3

https://infoq.cn/article/netty-high-performance

https://www.codercto.com/a/23502.html

你可能感兴趣的:(Java网络编程)