零拷贝技术

Python024

零拷贝技术,第1张

知识补充:

1、DMA是直接内存访问( Direct Memory Access 技术,早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。

2、每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

我们都听过 kafka 很快,其中一个原因是 kafka 使用零拷贝。看看从文件中读取数据并通过网络将数据传输到另一个程序的场景,在内部的复制操作,需要在用户模式和内核模式之间进行四次上下文切换,并且进行数据复制四次。

完成所有四个操作后,它将再次切换到用户模式。

回顾以上动作,其实发现实际上第二个和第三个数据拷贝是可以避免的。Java 类库通过 java.nio.channels 中的 transferTo() 方法在 UNIX 系统上执行零副本,使用零拷贝的应用程序请求内核直接将数据从磁盘文件复制到套接字,而不通过应用程序。零拷贝极大提高了应用程序性能,并减少了内核和用户模式之间的上下文切换次数。

我们看看 transferTo() 是如何复制数据的?

这里我们还需要 3 个副本和 2 个上下文切换。

但当前还没有达到零拷贝,如果底层网卡支持收集操作,可以进一步减少内核重复拷贝数据的操作。在 Linux 内核 2.4 及更高版本中,套接字缓冲区描述符支持该场景。

Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。拷贝技术,本质上讲就是通过减少非必要的内存拷贝以及上下文切换,来提高文件在通道间复制速度的一种技术。以本文中的transferTo()方法为例,通过该技术,可以将原来 四次内存间拷贝减少成两次,将四次上下文切换减少成两次 ,大大提高复制的速度。但零拷贝技术并非万能的,它有自己的使用场景,对于将大量数据从一个 I/O 通道复制到另一个通道的情况(例如 Web 服务器),都是合适的。

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

    说起零拷贝之前,先来了解下服务器中文件数据通过网络传输到客户端的流程。作为应用服务器,其中会有很多从磁盘中读取数据,然后应用程序对加载到内存中的数据进行处理,然后通过网卡发送给客户端,传统数据处理通过以下两个函数实现:

    在这个过程中,数据流转的大致过程如下:

     可以见到,在这个过程中发生了2次cpu copy和2次DMA copy,以及发生了数次cpu状态切换。 这个操作对于应用服务器来说很频繁,因此带来的开销也是非常大。

因此所谓的零拷贝就是,让其中的2次cpu拷贝省略掉,因为这两次cpu拷贝的数据其实已经在内存中,没有必要再让cpu参与进来进行数据的拷贝,浪费cpu。在大量文件读写的时候,这个优化带来的收益还是比较可观的。

零拷贝的实现方式有两种:

    mmap通过虚拟内存映射,让多个虚拟地址指向同一个物理内存地址,用户空间的虚拟地址和内核空间的虚拟地址指向同一个物理内存地址,这样用户空间和内核空间共享同一个内存数据。这样DMA引擎从磁盘上加载的数据不需要在内核空间和用户空间进行复制,减少了一次cpu拷贝。

    sendfile通过系统调用,并且规定了in_fd文件描述符必须是可以mmap的,sendfile只能将文件数据发送到socket中,sendfile减少了一次cpu状态的切换

    无论是mmap结合write方式还是sendfile方式都只是减少了一次cpu拷贝,而后DMA引擎还具有了收集功能,可以在内核缓存区发送到socket缓冲区的时候避免掉cpu复制,只是将缓冲区地址和数据长度发送给socket缓冲区,然后DMA引擎通过收集功能直接读取收集数据发送到网卡中。这里依赖DMA引擎的收集功能省略掉了最后一次cpu拷贝,到此才是真正的零拷贝。

     所谓的零拷贝就是避免数据在内核空间缓存区和用户空间缓缓冲区之间的复制,避免掉2次cpu复制,释放cpu。

    在RocketMq中采用的是mmap()结合write()方式来实现零拷贝。

    在java中还可以通过FileChannel.transferTo()来实现数据从文件描述符传输到socket中,它的底层是通过sendfile系统调用来实现。