浅浅聊下零拷贝

理解零拷贝

所谓零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。要聊零拷贝就需要先了解一些技术,如:内核态与用户态、DMA技术、传统拷贝。

内核态与用户态

在 Linux 操作系统中,内核态和用户态是两种不同的运行模式,它们在权限、资源访问和执行环境等方面存在显著差异。

  • 内核态:也称为特权模式,拥有最高的权限和对系统资源的完全访问能力。当操作系统内核执行关键任务,如管理硬件设备、处理中断、执行内存管理和进程调度等操作时,就处于内核态。内核态可以直接访问和操作硬件资源,执行所有的指令,并对整个系统的稳定性和安全性负责。

  • 用户态:又称非特权模式,是应用程序运行的常见模式。在用户态下,应用程序受到严格的权限限制,只能访问自己的内存空间和有限的系统资源。它不能直接访问硬件设备,也不能执行一些特权指令。如果应用程序需要执行特权操作或访问受保护的资源,就必须通过系统调用切换到内核态来完成。

内核态和用户态的切换是通过系统调用中断来实现的。当应用程序发起系统调用,如打开文件、创建进程等,处理器会从用户态切换到内核态,执行内核中的相应代码来完成请求,完成后再切换回用户态继续应用程序的执行。这种分离模式有助于保障系统的稳定性和安全性,防止用户程序的错误或恶意行为直接影响到整个系统的正常运行。

DMA(Direct Memory Access, 直接内存访问)

在DMA技术出现之前,通常由 CPU 负责数据在不同设备(如磁盘、网卡等)和内存之间的传输,这会大量占用 CPU 的处理时间,降低了系统的整体性能。而 DMA 技术的出现改变了这一状况。

  • DMA技术: 是一种允许某些硬件子系统独立于 CPU 直接访问计算机系统主内存的技术。

DMA 控制器可以在无需 CPU 干预的情况下,直接控制数据在内存和外部设备之间的传输。例如,当从磁盘读取数据到内存时,DMA 控制器可以接管总线控制权,直接将数据从磁盘控制器缓冲区传输到内存,传输完成后再通知 CPU 进行后续处理。当然DMA技术还广泛应用于各种需要大量数据传输的场景,如磁盘 I/O、网络通信、音频和视频处理等。

传统拷贝

以从磁盘中读取文件并通过网络传输为例。大概可以分为两大步骤:

  1. 从磁盘中读取文件;
  2. 将文件通过网络发送。

但是实际上流程却并非如此简单,从硬件、操作系统的内核态和用户态去分析可以大体上分为几个流程:

  1. 用户程序调用read函数,发起IO请求,此时CPU从用户态陷入内核态;
  2. 操作系统通过DMA控制器从磁盘中拷贝数据,并将数据存储到内核缓冲区;
  3. CPU 把内核缓冲区的数据,拷贝到用户缓冲区,同时CPU上下文从内核态回到用户态;
  4. 用户程序处理数据,再按照同样的流程将数据从用户缓冲区传输到socket缓冲区,最终再到网卡完成数据发送。
    传统拷贝流程图
  • 数据拷贝次数:2 次 DMA 拷贝,2 次 CPU 拷贝
  • CPU 切换次数:4 次用户态和内核态的切换

而实际 IO 读写,有时候需要进行 IO 中断,同时也需要 CPU 响应中断,拷贝次数和切换次数比预期的还要多,以至于在完成数据传输的时候,传输速度总是不尽人意。

零拷贝的实现方式

mmap 内存映射拷贝

从磁盘中读取文件并通过网络传输的场景中,其实不难发现用户缓冲区其实只是相当于一个中间人,从内核缓冲区拿到数据转手又传输到socket缓冲中。因此是可以避免将数据拷贝到用户缓冲区的。因此在linux操作系统中提供了一种 mmap(内存映射) 的概念来避免数据的拷贝。

  • mmap(内存映射): 本质上是一段特殊的虚拟内存,用于将文件逻辑上一段连续的数据(物理上可以不连续存储)映射为连续内存。mmap 由操作系统负责管理,对同一个文件地址的映射将被所有线程共享,操作系统确保线程安全以及线程可见性。

mmap 内存映射拷贝
数据拷贝次数:2 次 DMA 拷贝,1 次 CPU 拷贝
CPU 切换次数:4 次用户态和内核态的切换

整个过程省掉了数据在内核缓冲区和用户缓冲区之间的 CPU 拷贝环节,在实际的应用中,对资源的拷贝能提升不少。

sendfile 拷贝

在 Linux 2.1 内核版本中,引入了一个系统调用方法:sendfile。当调用 sendfile函数时,DMA 将磁盘数据复制到内核缓冲区;然后将内核缓冲区的数据直接拷贝到 socket 缓冲区;最后利用 DMA 将 socket 缓冲区的数据通过网卡发送出去。
sendfile拷贝
数据拷贝次数:2 次 DMA 拷贝,1 次 CPU 拷贝
CPU 切换次数:2 次用户态和内核态的切换

相比 mmap 内存映射方式,Linux 2.1 内核版本中 sendfile 拷贝流程省掉了 2 次用户态和内核态的切换,同时内核缓冲区和用户缓冲区也无需建立内存映射,对资源的拷贝能提升不少。

SG-DMA 技术拷贝(sendfile With DMA scatter/gather)

在 Linux 2.4 内核版本中,对 sendfile 系统方法做了优化升级,引入 SG-DMA 技术,需要 DMA 控制器支持。其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点来实现数据拷贝,可以不需要 CPU 参与拷贝,真正的实现0拷贝。
SG-DMA 技术拷贝
数据拷贝次数:2 次 DMA 拷贝,0 次 CPU 拷贝
CPU 切换次数:2 次用户态和内核态的切换

可以发现,基于 SG-DMA 技术实现的拷贝,其中 2 次数据拷贝都是 DMA 拷贝,全程都没有通过 CPU 来拷贝数据,所有的数据都是通过 DMA 来进行传输的,这就是操作系统真正意义上的零拷贝(Zero-copy) 技术,相比其他拷贝方式,传输效率最佳。

splice 零拷贝

在 Linux 2.6.17 内核版本中,引入了 splice 系统调用方法,和 sendfile 方法不同的是,splice 不需要硬件支持。它将数据从磁盘读取到 OS 内核缓冲区后,内核缓冲区和 socket 缓冲区之间建立管道来传输数据,避免了两者之间的 CPU 拷贝操作。
splice 零拷贝
数据拷贝次数:2 次 DMA 拷贝,0 次 CPU 拷贝
CPU 切换次数:2 次用户态和内核态的切换

Linux 系统 splice 方法逻辑拷贝,也是操作系统真正意义上的零拷贝,因为它不需要 DMA 的支持也可以实现。

几种拷贝机制对比

拷贝机制系统调用CPU拷贝次数DMA拷贝次数上下文切换次数特点
传统拷贝read/write224消耗系统资源多,拷贝效率低
mmapmmap/write125降低了用户缓冲区与内核缓冲区的数据拷
sendfilesendfile122
SG-DMAsendfile022需要DMA控制器支持,CPU不参与数据拷贝
splicesplice022CPU不参与数据拷贝,真正的零拷贝

零拷贝的应用场景

通常情况下,我们现在的大多数高级语言会对操作系统的底层函数进行封装来降低使用者的技术复杂度,所以在高级语言中很难找到原生态的系统调用函数。

在Java中,NIO 中提供的 FileChannel 拥有 transferTo 和 transferFrom 两个方法,可直接把FileChannel 中的数据拷贝到另外一个 Channel,或者直接把另外一个 Channel 中的数据拷贝到FileChannel。该接口常被用于高效的网络 / 文提供的件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于 Java IO 中提供的方法。

而其中最经典的莫过于Kafka了。Kafka两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据。Producer生产的数据持久化到broker,broker里采用mmap文件映射,实现顺序的快速写入;Consumer从broker读取数据,broker里采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。读取数据并发送的核心源代码为:FileChannel.transferTo(FileChannel)

关于零拷贝的个人感悟

  1. 数据的处理
    如果需要发送的数据需要经过各种处理,那么此时是不太适合将数据继续存储在内核区域的。比如Netty在处理这种问题的时候是自己开辟了一个内存缓冲区(ByteBuffer),可以是直接内存(DirecrtBytebuf)也可以是堆内存(HeapBytebuf)。不管是什么内存它都是处于用户态的,但这个内存缓冲区是可重用的,性能也能得到一定的保证。因此我们有时候不必追求于零拷贝,有时候只需要借助于零拷贝的思想,就是复用内存,避免拷贝发生的性能损耗。
  2. 内存的限制
    如果一个文件很大,那么一次性将文件读取到内核缓冲区的做法明显是不合理的。那么自然而然的就会产生分而治之的想法,放不了大块就切割成小块,逐个发送,这对内存的负担也就不会太大了。这也是为什么文件切片上传或下载的思想源头之一吧。
  3. 避免深究底层细节
    现在大部分高级语言在调用操作系统的核心功能时都会进行一系列的封装,本意就是降低技术门槛。作为一个技术人,大多数情况下其实只需要知道有这么回事即可,不必过于追求细节,等有需要的时候再去找相关资料研究即可。比如:golang中其实也有零拷贝,并且封装相对比较原始。系统调用的相关包都在syscall包中,但我们通常只需要知道有这么个函数即可,要用的时候再去研究,而不需要深究其底层细节,因为我们大多数人是使用技术去为业务产生价值的人,所以我们需要考虑一个投入回报比例,当然兴趣除外。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值