Netty分享之ByteBuf零拷贝

我们或多或少了解过一些零拷贝的概念,而零拷贝也正是netty能够实现高性能的原因之一,因此我们有必要深入了解netty的零拷贝。但是在深入了解netty的零拷贝之前,让我们先来了解一下操作系统(以Linux为例)中的零拷贝。要了解操作系统的零拷贝,我们首先要先了解两个概念:DMA、用户态与内核态


什么是DMA

DMA(Direct Memory Access),也就是直接内存存取。

DMA是一种能够允许输入输出设备(input/output IO)直接访问主内存,绕开CPU快速操作内存的方法,这个过程由一个称为DMA控制器的芯片来管理。

在早期的老式电脑中,一共有4个DMA通道,编号从0到3。后来引入16位工业标准体系结构(ISA)扩展总线后,又新增了4个DMA通道,编号从4到7。ISA是IBM兼容计算机的总线标准,他允许设备以更快的速度启动事务(总线控制)。每个DMA每秒传输大约2兆字节的数据。

我们知道计算机的系统资源是用于硬件和软件之间进行通信的。一共有四种类型的系统资源:

  • 输入输出地址
  • 内存地址
  • 中断请求号(IRQ)
  • 直接内存存取(DMA)通道

DMA通道则是用于外围设备和系统存储器之间进行数据传输。这四种系统资源都依赖于总线上的某些线路。一些线路用于IRQ,一些线路用于地址交换(I/O地址和内存地址)而另一些则是用于DMA通道。

DMA通道使设备能够在不使CPU过载的情况下传输数据。如果没有DMA通道,CPU将从来自I/O设备的外围总线上一块一块的拷贝数据。如果在读写过程中使用外围总线将占用CPU,并且在完成操作之前都不允许CPU执行其他工作。

通过DMA,CPU可以在执行数据传输时处理其他任务。数据的传输首先由CPU发起,在DMA通道和I/O设备之间传输数据时,CPU可以执行其他任务。当数据传输完成时,DMA控制器会向CPU发送一个中断请求。


什么是用户态与内核态

Linux操作系统的体系架构分为用户态和内核态,内核从本质上看是一种软件,专门用来控制计算机的硬件资源,并提供上层应用程序运行所需的环境。用户态就是上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。以下是一个简单的内核态与用户态的关系,内核态在“下层”,控制着计算机的硬件资源,用户态在“上层”:

+--------------------------+
|       +----------+       |
|       |  kernel  |       |
|       +----------+       |
|           user           |
+--------------------------+

Copy

当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权限执行的工作时就会切换到内核态,比如当应用程序需要读取一个文件时,则需要把数据从磁盘上读取到内核态中的内核缓冲区中,然后再从内核缓存区中拷贝到用户态的用户空间缓冲区中。可以用下面的图形简单的表示这个过程:

 [ file ]--- DMA -->[ kernel buffer ]--- copy -->[ user buffer ]

Copy

如果应用程序需要把一个文件发送到远程服务器中去,那么除了这两次copy之外,还有另外两次copy,分别是从用户态拷贝到内核态的socket缓冲区,然后从socket缓冲区通过DMA写入到网络,整个过程如下图所示:

 [ file ]--- DMA -->[ kernel buffer ]--- copy -->[ user buffer ]
                                                        |
                                                        |
[ network ]<-- DMA --[ socket buffer ]<-- copy --[ user buffer ]                                                       

Copy

操作系统通过诸如设备驱动程序、文件系统和网络协议栈对零拷贝的支持,极大地提高了应用程序的性能,并且更高效地利用了系统资源。通过让CPU在数据拷贝的过程中,能够脱身去处理其他任务,从而提高了性能。此外,零拷贝操作减少了用户态和内核态之间耗时的模式切换和数据拷贝。

在Linux中,系统为我们提供了一个mmap()方法,应用程序调用mmap(),磁盘上的数据会通过DMA被拷贝到内核缓冲区,然后操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。然后将数据向网络中写时,只需要把数据从这块共享的内核缓冲区中拷贝到socket缓冲区中去就行了,这些操作都发生在内核态,最后,socket缓冲区再把数据发到网卡去,这样原本需要四次的拷贝,现在只需要三次拷贝了:

 [ file ]--- DMA -->[ kernel buffer ]        +----------+
                               |             |          |
                              copy           |   mmap   |
                               |             |          |
[ network ]<-- DMA ---[ socket buffer ]      +----------+                                          

Copy

java中的MappedByteBuffer类底层就是使用的mmap技术。MappedByteBuffer将文件映射为内存,访问的时候通过缺页机制将数据读入内存。


Netty中的零拷贝

其实netty中并没有实现真正的零拷贝,netty中的零拷贝更多的应该理解为少拷贝或者说复用(reuse),操作系统的零拷贝是避免了CPU将数据从一个内存区域拷贝到另一个内存区域,而netty中的数据操作全部是在用户态。当真正要通过netty将数据发送到网络时,仍然需要将数据从用户态拷贝到内核态,此时就无法做到真正的零拷贝了。

Netty的零拷贝(或者说ByteBuf的复用)主要体现在以下几个方面:

  • DirectByteBuf通过直接在堆外分配内存的方式,避免了数据从堆内拷贝到堆外的过程
  • 通过组合ByteBuf类:即CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf, 而不需要进行数据拷贝
  • 通过各种包装方法, 将 byte[]、ByteBuf、ByteBuffer等包装成一个ByteBuf对象,而不需要进行数据的拷贝
  • 通过slice方法, 将一个ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免了内存的拷贝,这在需要进行拆包操作时非常管用
  • 通过FileRegion包装的FileChannel.tranferTo方法进行文件传输时, 可以直接将文件缓冲区的数据发送到目标Channel, 减少了通过循环write方式导致的内存拷贝。但是这种方式是需要得到操作系统的零拷贝的支持的,如果netty所运行的操作系统不支持零拷贝的特性,则netty仍然无法做到零拷贝


评论区
Rick ©2018