零拷贝

347 阅读7分钟

20210619205023vz3f2o

原文地址:www.programmersought.com/article/723…

零拷贝(Zero Copy)是一个耳熟能详的名词,很多高性能网络框架如Netty,Kafka,RocketMQ都将零拷贝作为其特点。那么究竟什么是零拷贝?

Zero copy

维基百科对零拷贝的定义如下:

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

“零拷贝”描述了计算机操作,其中 CPU 不执行将数据从一个内存区域复制到另一个内存区域的任务。这通常用于在通过网络传输文件时节省 CPU 周期和内存带宽。

Zero copy in Linux system

术语

  • 内核空间

    计算机内存分为用户空间和内核空间。内核空间运行OS内核代码,可以访问所有内存、机器指令和硬件资源,拥有最高权限。

  • 用户空间

    内核外部的所有空间,用于正常的用户进程操作。用户空间的进程不能访问内核空间,只能通过内核系统调用(系统调用)公开的接口访问内核的一小部分。如果用户进程请求执行系统调用,则需要向内核发送系统中断(软件中断),内核会调度相应的中断处理程序来处理请求。

  • DMA

    直接内存访问(DMA)是为了响应CPU与硬盘之间的速度大小不匹配,它允许某些硬件子系统独立于CPU主存进行访问。

    如果没有DMA、CPU执行IO操作的整个过程被阻塞,无法执行其他工作,这将导致计算机陷入挂起。如果有DMA干预,IO过程就变成这样了:CPU启动DMA传输过程中,可以执行其他操作;DMA控制器(DMAC)传输完成后,会给CPU发送一个中断信号,然后CPU就可以对传输的数据进行处理了。

Traditional network transmission

网络IO的一个常见场景是从硬盘读取文件,通过网卡发送到网络。下面是一个简单的伪代码:

// read data from hard disk
File.read(fileDesc, buf, len);
// Send data to the network
Socket.write(socket, buf, len);

在代码层面,这是一个非常简单的操作,但在系统层面,让我们看看幕后发生了什么:

20210619211045SSoeFe

  1. User initiated read() System call (syscall), request hard disk data. At this point, it will happen once Context switch(context switch)。
  2. DMA Read the file from the hard disk. At this time, a copy is generated:hard disk–>DMA buffer
  3. DMA Copy data toUser spaceread() The call returns. At this time, it happened once Context switch And a data copy: DMA buffer–>User space
  4. User initiated write() System call, request to send data. This happens once Context switch And a data copy: User space–>DMA buffer
  5. DMA Copy the data to the network card for network transmission. The fourth data copy occurs at this time:DMA buffer–>Socket buffer
  6. write() The call returns and happens again Context switch

数据流如下:

20210619211517AlprCv

可以发现,涉及4次上下文切换和4次数据拷贝。对于简单的网络文件发送,有很多不必要的开销。

sendfile transmission

对于上面的场景,可以发现从DMA缓冲区到用户空间和从用户空间到套接字缓冲区两次CPU复制是完全没有必要的,零拷贝由此诞生。针对这种情况,Linux内核提供了sendfile系统调用。

如果使用sendfile()执行上述请求,系统流程可以简化如下:

20210619212016ieRHQD

sendfile()系统调用,可以在DMA中实现数据的内部复制,而不需要将数据复制到用户空间。因此,上下文切换次数减少到2次,数据拷贝次数减少到3次。

Here is a question: whyDMAThere will be a copy inside (this copy needsCPUparticipate)?

This is because the early network cards required the data to be sent to be continuous in physical space, so there wasSocket Buffer. But if the network card itself supports scatter-gather, that is, it can gather and send data from discontinuous memory addresses, then it can be further optimized.

Network card support scatter-gather of sendfile transmission

inLinuxThis has been optimized after kernel version 2.4. If the computer network card supports the collection operation,sendfile()Operation can be omittedSocket BufferThe data is copied, instead, the descriptors of the data location and length are directly passed toSocket Buffer

20210619212541y7show

在网卡的支持下,上下文切换次数为2次,数据复制次数也减少到2次。两次数据复制是必须的,即数据在内存中复制已经完全避免了。

对于从硬盘向网络发送文件的场景,如果网卡支持采集操作,那么sendfile()系统调用,真正意义上的零拷贝。

Memory mapping (mmap)

在“通过互联网发送文件”的情况下,使用sendfile()系统调用可以极大地提高性能(根据测试,吞吐量可以达到传统方法的三倍)。但有一个缺点是,它只支持“读->发送”的“连续操作”,所以,sendfile()一般用来处理一些静态的网络资源,如果要对数据进行额外的操作,它是无能为力的。内存映射(Memory mapping)为此提供了一种解决方案。

mmap是一种内存映射文件的方法。它可以将文件映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中虚拟地址的对应。这样,用户进程就可以使用指针对这块内存进行读写操作,内核空间对这块内存的修改直接反映在用户空间中。

简而言之,mmap实现了用户空间和内核空间的数据共享。可以猜到,如果使用mmap 系统调用,上述场景的步骤如下:

20210619213243qeFckV

数据流如下:

20210619213506rcq3a0

与传统方式相比,mmap它保存了一份数据拷贝,这也可以从广义上称为零拷贝(Zero Copy)。同时,它还允许用户自定义数据操作,这与发送文件相比,其优势在于。

Zero copy in JDK NIO

从1.4版开始,JDK引入了NIO,提供了正确的Zero Copy支持。由于JVM运行在上述操作系统上,其功能只是对系统底层API的封装,如果操作系统不支持Zero Copy(mmap/sendfile),那么JVM就无能为力了。JDK正确的Zero Copy Package,主要体现在FileChannel这个类上。

map()

map()方法如下:

public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {

    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
}

map()方法描述如下:

将此通道文件的一个区域直接映射到内存中。对于大多数操作系统来说,将文件映射到内存比通过通常的读写方法读取或写入数十KB的数据更昂贵。从性能的角度来看,通常只值得将相对较大的文件映射到内存中

map()方法返回MappdByteBufferDirectByteBuffer就是它的子类别。它引用一个独立于JVM内存的块,它位于GC之外,由该机制控制,您需要自己管理创建和销毁操作

transferTo()

transferTo()方法如下:

public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {

    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
}

transferTo()方法描述如下:

将字节从此通道的文件传输到给定的可写字节通道时,此方法可能比从此通道读取并写入目标通道的简单循环高效得多。

许多操作系统可以直接将字节从文件系统缓存传输到目标通道,而无需实际复制它们。

请注意,由于sendfile()只适用于套接字缓冲区发送数据,因此通过Zero Copy技术提高性能只能用于通过网络发送数据的场景。

那是什么意思?如果您只是使用Transfer To()将数据从硬盘上的一个文件写入到另一个文件,则不会产生任何性能提升效果。

Zero copy in Netty

// todo