零拷贝(零拷贝的原理)

软件教程 2023.01.03 181

目录:

[转]浅谈Linux下的零拷贝机制

维基上是这么描述零拷贝的:零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。

减少甚至完全避免不必要的CPU拷贝,从而让CPU解脱出来去执行其他的任务

减少内存带宽的占用

通常零拷贝技术还能够减少用户空间和操作系统内核空间之间的上下文切换

从Linux系统上看,除了引导系统的BIN区,整个内存空间主要被分成两个部分: 内核空间(Kernel space) 、 用户空间(User space) 。“用户空间”和“内核空间”的空间、操作权限以及作用都是不一样的。

内核空间是Linux自身使用的内存空间,主要提供给程序调度、内存分配、连接硬件资源等程序逻辑使用;

用户空间则是提供给各个进程的主要空间。用户空间不具有访问内核空间资源的权限,因此如果应用程序需要使用到内核空间的资源,则需要通过系统调用来完成:从用户空间切换到内核空间,然后在完成相关操作后再从内核空间切换回用户空间。

① 直接 I/O:对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,但是硬件上的数据不会拷贝一份到内核空间,而是直接拷贝至了用户空间,因此直接I/O不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。

② 在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的CPU拷贝,以及数据在系统内核空间内的CPU拷贝。本文主要讨论的就是该方式下的零拷贝机制。

③ copy-on-write(写时复制技术):在某些情况下,Linux操作系统的内核空间缓冲区可能被多个应用程序所共享,操作系统有可能会将用户空间缓冲区地址映射到内核空间缓存区中。当应用程序需要对共享的数据进行修改的时候,才需要真正地拷贝数据到应用程序的用户空间缓冲区中,并且对自己用户空间的缓冲区的数据进行修改不会影响到其他共享数据的应用程序。所以,如果应用程序不需要对数据进行任何修改的话,就不会存在数据从系统内核空间缓冲区拷贝到用户空间缓冲区的操作。

下面我们通过一个Java非常常见的应用场景:将系统中的文件发送到远端(该流程涉及:磁盘上文件 —— 内存(字节数组) —— 传输给用户/网络)来详细展开传统I/O操作和通过零拷贝来实现的I/O操作。

① 发出read系统调用:导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将文件中的数据从磁盘上读取到内核空间缓冲区(第一次拷贝: hard drive —— kernel buffer)。

② 将内核空间缓冲区的数据拷贝到用户空间缓冲区(第二次拷贝: kernel buffer —— user buffer),然后read系统调用返回。而系统调用的返回又会导致一次内核空间到用户空间的上下文切换(第二次上下文切换)。

③ 发出write系统调用:导致用户空间到内核空间的上下文切换(第三次上下文切换)。将用户空间缓冲区中的数据拷贝到内核空间中与socket相关联的缓冲区中(即,第②步中从内核空间缓冲区拷贝而来的数据原封不动的再次拷贝到内核空间的socket缓冲区中。)(第三次拷贝: user buffer —— socket buffer)。

④ write系统调用返回,导致内核空间到用户空间的再次上下文切换(第四次上下文切换)。通过DMA引擎将内核缓冲区中的数据传递到协议引擎(第四次拷贝: socket buffer —— protocol engine),这次拷贝是一个独立且异步的过程。

Q: 你可能会问独立和异步这是什么意思?难道是调用会在数据被传输前返回?

A: 事实上调用的返回并不保证数据被传输;它甚至不保证传输的开始。它只是意味着将我们要发送的数据放入到了一个待发送的队列中,在我们之前可能有许多数据包在排队。除非驱动器或硬件实现优先级环或队列,否则数据是以先进先出的方式传输的。

总的来说,传统的I/O操作进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。其中4次数据拷贝中包括了2次DMA拷贝和2次CPU拷贝。

Q: 传统I/O模式为什么将数据从磁盘读取到内核空间缓冲区,然后再将数据从内核空间缓冲区拷贝到用户空间缓冲区了?为什么不直接将数据从磁盘读取到用户空间缓冲区就好?

A: 传统I/O模式之所以将数据从磁盘读取到内核空间缓冲区而不是直接读取到用户空间缓冲区,是为了减少磁盘I/O操作以此来提高性能。因为OS会根据局部性原理在一次read()系统调用的时候预读取更多的文件数据到内核空间缓冲区中,这样当下一次read()系统调用的时候发现要读取的数据已经存在于内核空间缓冲区中的时候只要直接拷贝数据到用户空间缓冲区中即可,无需再进行一次低效的磁盘I/O操作(注意:磁盘I/O操作的速度比直接访问内存慢了好几个数量级)。

Q: 既然系统内核缓冲区能够减少磁盘I/O操作,那么我们经常使用的BufferedInputStream缓冲区又是用来干啥的?

A: BufferedInputStream的作用是会根据情况自动为我们预取更多的数据到它自己维护的一个内部字节数据缓冲区中,这样做能够减少系统调用的次数以此来提供性能。

总的来说内核空间缓冲区的一大用处是为了减少磁盘I/O操作,因为它会从磁盘中预读更多的数据到缓冲区中。而BufferedInputStream的用处是减少“系统调用”。

DMA(Direct Memory Access) ———— 直接内存访问 :DMA是允许外设组件将I/O数据直接传送到主存储器中并且传输不需要CPU的参与,以此将CPU解放出来去完成其他的事情。

而用户空间与内核空间之间的数据传输并没有类似DMA这种可以不需要CPU参与的传输工具,因此用户空间与内核空间之间的数据传输是需要CPU全程参与的。所有也就有了通过零拷贝技术来减少和避免不必要的CPU数据拷贝过程。

① 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive —— kernel buffer)。然后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer —— socket buffer)。

② sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer —— protocol engine)

总的来说,通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。

Q: 但通过是这里还是存在着一次CPU拷贝操作,即,kernel buffer —— socket buffer。是否有办法将该拷贝操作也取消掉了?

A: 有的。但这需要底层操作系统的支持。从Linux 2.4版本开始,操作系统底层提供了scatter/gather这种DMA的方式来从内核空间缓冲区中将数据直接读取到协议引擎中,而无需将内核空间缓冲区中的数据再拷贝一份到内核空间socket相关联的缓冲区中。

从Linux 2.4版本开始,操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这样一来待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。那么从文件中读出的数据就根本不需要被拷贝到socket缓冲区中去,只是需要将缓冲区描述符添加到socket缓冲区中去,DMA收集操作会根据缓冲区描述符中的信息将内核空间中的数据直接拷贝到协议引擎中。

① 发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive —— kernel buffer)。

② 没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。

③ sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer —— protocol engine),这样就避免了最后一次CPU数据拷贝。

总的来说,带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。

在linux2.6.33版本之前 sendfile指支持文件到套接字之间传输数据,即in_fd相当于一个支持mmap的文件,out_fd必须是一个socket。但从linux2.6.33版本开始,out_fd可以是任意类型文件描述符。所以从linux2.6.33版本开始sendfile可以支持“文件到文件”和“文件到套接字”之间的数据传输。

Q: 对于上面的第三点,如果我们需要对数据进行操作该怎么办了?

A: Linux提供了mmap零拷贝来实现我们的需求。

mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。

① 发出mmap系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。通过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive —— kernel buffer)。

② mmap系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不需要将数据从内核空间拷贝到用户空间。因为用户空间和内核空间共享了这个缓冲区数据,所以用户空间就可以像在操作自己缓冲区中数据一般操作这个由内核空间共享的缓冲区数据。

③ 发出write系统调用,导致用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer —— socket buffer)。

④ write系统调用返回,导致内核空间到用户空间的上下文切换(第四次上下文切换)。通过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer —— protocol engine)

总的来说,通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。

FileChannel中大量使用了我们上面所提及的零拷贝技术。

FileChannel的map方法会返回一个MappedByteBuffer。MappedByteBuffer是一个直接字节缓冲器,该缓冲器的内存是一个文件的内存映射区域。map方法底层是通过mmap实现的,因此将文件内存从磁盘读取到内核缓冲区后,用户空间和内核空间共享该缓冲区。

MappedByteBuffer内存映射文件是一种允许Java程序直接从内存访问的一种特殊的文件。我们可以将整个文件或者整个文件的一部分映射到内存当中,那么接下来是由操作系统来进行相关的页面请求并将内存的修改写入到文件当中。我们的应用程序只需要处理内存的数据,这样可以实现非常迅速的I/O操作。

只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常

读写模式表明,对结果对缓冲区所做的修改将最终广播到文件。但这个修改可能会也可能不会被其他映射了相同文件程序可见。

私有模式来说,对结果缓冲区的修改将不会被广播到文件并且也不会对其他映射了相同文件的程序可见。取而代之的是,它将导致被修改部分缓冲区独自拷贝一份到用户空间。这便是OS的“copy on write”原则。

如果操作系统底层支持的话transferTo、transferFrom也会使用相关的零拷贝技术来实现数据的传输。所以,这里是否使用零拷贝必须依赖于底层的系统实现。

转自:

零拷贝Zero-copy技术详解

普通模式数据交互分为仅CPU和CPUDMA两种方式。

read流程:

小结:

我们可以看出仅CPU方式需要跟硬件交互,很耗CPU,这是在浪费CPU资源。

于是就有了DMA(Direct Memory Access, 直接内存访问)来分担CPU的活。

有必要先讲一下DMA是什么,DMA是一种硬件设备绕开CPU独立直接访问内存的机制。所以DMA在一定程度上解放了CPU,与硬件交互的工作让硬件直接自己做了,提高了CPU效率。 支持DMA的有网卡、声卡、显卡、磁盘控制器等。

CPUDMA方式对比仅CPU方式的区别就是,CPU不再和磁盘直接交互,而是DMA和磁盘交互,使得读写运行效率更高。

并且在DMA读写硬件时,CPU可以执行其他任务。

read流程:

小结:

通过上面两种普通模式数据交互的方式发现,如果我们执行read和write操作,会经历四次数据拷贝和四次空间切换。基于这两点我们可以做如下优化:

基于上面两点优化,实现了这些零拷贝技术: mmap+write、sendfile、sendfile+DMA收集、splice等。

mmap即memory map,也就是内存映射。我在 mmap的使用 一文中详细介绍了mmap的用法、特点、注意事项等信息,感兴趣的同学可以去看看。

mmap+write流程:

小结:

不管是ready+write还是mmap+write,都是使用两个接口来做数据传输,按照第二点优化思路,我们可以用一个系统调用接口来实现,这样空间切换就会从四次缩为两次。

sendfile是Linux 内核2.1版本中被引入,与mmap+write一样,sendfile是从内核缓冲区拷贝到socket缓冲区,流程如下:

小结:

从 Linux 内核 2.4 版本开始起,sendfile() 系统调用的过程发生了点变化,具体过程如下:

这种方式才是实现了真正的零拷贝,真正的解放了CPU。但是这种方式需要硬件DMA控制器的配合。流程图示如下:

小结:

小结:

为什么磁盘数据拷贝到网卡中,需要经过内核缓冲区呢?原因是磁盘的读写速度太慢。

内核缓冲区工作:

大文件的缓存命中率不高,并且可能内核缓存区被大文件占据,而导致其他的热点小文件无法利用内核缓存区。

所以适用内核缓冲器时,不适合大文件传输。 也就是说零拷贝技术不适合大文件传输。

我们了解了为什么使用内核缓冲区后,就能自然而然的想到,如果可以忍受磁盘读写速度就可以避免使用内核缓冲区。

异步IO就是可以忍受磁盘读写速度,因为线程不用等待异步IO的执行结果。

所以异步IO可以做到直接从磁盘缓冲区拷贝到用户缓冲区,适用于大文件传输。

本文介绍了CPU和CPUDMA两种数据传输的流程:

然后介绍了优化的点分别是减少数据拷贝次数和介绍空间切换次数,于是引出了零拷贝技术: mmap+write、sendfile、sendfile+DMA收集、splice等。

最后介绍了内核缓冲区的作用,也引出了异步IO可以处理大文件传输工作。

wiki Page_cache

wiki Zero-copy

wiki Mmap

man2 sendfile

wiki Splice

0拷贝和2拷贝是

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

针对操作系统中的设备驱动程序、文件系统以及网络协议堆栈而出现的各种零拷贝技术极大地提升了特定应用程序的性能,并且使得这些应用程序可以更加有效地利用系统资源。这种性能的提升就是通过在数据拷贝进行的同时,允许 CPU 执行其他的任务来实现的。

零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率。而且,零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。进行大量的数据拷贝操作其实是一件简单的任务,从操作系统的角度来说,如果 CPU 一直被占用着去执行这项简单的任务,那么这将会是很浪费资源的;如果有其他比较简单的系统部件可以代劳这件事情,从而使得 CPU 解脱出来可以做别的事情,那么系统资源的利用则会更加有效。综上所述,零拷贝技术的目标可以概括如下:

避免数据拷贝

①避免操作系统内核缓冲区之间进行数据拷贝操作。

②避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。

本文转载自互联网,如有侵权,联系删除

相关推荐