Linux下的零拷贝

零拷贝是什么?

维基百科对“零拷贝”是这样描述的:

"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周期和内存带宽。简单来说,零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

为什么需要零拷贝技术?

通常我们会有这样的需求:将本地磁盘上的一个文件通过网络发送给远端的另一个服务。在传统的I/O中,我们通过一张图来看一下操作系统都会发生什么:
Linux下的零拷贝

  1. 发出read()系统调用,这时处理器会从用户空间切换至内核空间;

  2. 向磁盘请求数据;

  3. 通过DMA将文件从磁盘上读取到内核空间缓冲区;

  4. read()系统调用返回,将数据从内核空间缓冲区拷贝至用户空间缓冲区,这时候处理器会从内核空间切换至用户空间;

  5. 发出write()系统调用,并将数据从用户空间缓冲区拷贝至目标socket 在内核空间的缓冲区,这时候处理器会从用户空间切换至内核空间;

  6. write()调用返回;

  7. 通过DMA将数据从内核空间缓冲区中拷贝至协议引擎(该操作是独立且异步的)。

总的来说:传统的I/O操作在整个过程中将会产生4次上下文切换和4次数据拷贝。

Q:有人可能会问, 为什么write()调用会先返回,难道他会在数据传输前返回?
A:事实上调用的返回并不保证数据被传输,甚至他并不保证传输的开始,只是意味着以太网驱动程序在其传输队列中有空位,并已经接受我们的将要传输的数据。在我们之前很可能还有很多数据包在排除。除非驱动程序或硬件实现优先级环或队列,否则数据都将以先进先出的方式被传输。

了解了传统I/O的操作后,我们再来观察一下整个过程,我们会注意到第二次和第三次数据拷贝是完全没有意义的,应用程序仅仅缓存了一下数据就又原封不动的把它发送给了目标socket 缓冲区。而且这两次拷贝是需要CPU全程参与的,从操作系统的角度来说,如果 CPU 一直被占用着去执行这项简单的任务,那么这将会是很浪费资源的;如果有其他比较简单的系统部件可以代劳这件事情,从而使得 CPU 解脱出来可以做其他的事情,那么系统资源的利用则会更加有效。

“零拷贝”正是通过消除这些多余的拷贝来提升性能的。在数据传输的过程中,避免数据在内核空间缓冲区和用户空间缓冲区之间进行拷贝,以及数据在内核空间缓冲区内的CPU拷贝。


零拷贝的实现机制

Linux 中提供类似的系统调用主要有 sendfile()、mmap() 和splice()(本文对该系统调用暂不做讨论)。

通过sendfile()实现的零拷贝

sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个本地文件之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数。为了更好的说明,请看下图:
Linux下的零拷贝

  1. 发出sendfile()系统调用,这时处理器会从用户空间切换至内核空间;

  2. 向磁盘请求数据;

  3. 通过DMA将文件从磁盘上读取到内核空间缓冲区;

  4. 将数据从内核空间缓冲区拷贝到目标socket缓冲区;

  5. Sendfile()返回,这时处理器从内核空间切换至用户空间;

  6. 通过DMA将数据从目标socket缓冲区拷贝至协议引擎。

总结一下这种实现,整个过程产生了2次上下文切换和3次数据拷贝(其中2次DMA拷贝和1次CPU拷贝)。

该实现虽然减少了2次上下文切换,但仍然还有1次CPU拷贝。那这次拷贝是不是也可以省掉呢?答案是肯定的。但是需要底层操作系统的一些支持。那就是带有DMA收集功能的sendfile实现的零拷贝。

带有DMA收集功能的sendfile实现的零拷贝

从Linux2.4开始,操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这就意味着等待传输的数据不需要在连续存储器中,它可以分散在不同的内存位置。那这样一来,从文件中读出的数据就不必拷贝至目标socket的缓冲区中,只需要将缓冲区描述符添加到目标socket的缓冲区中,DMA收集操作会根据缓冲区描述符中的信息将内核空间缓冲区中的数据读取到协议引擎。这种方法不仅减少了上下文切换、还减少了由CPU参与的数据拷贝。为了更好的理解这种方法所涉及的操作,请看下图:

Linux下的零拷贝

  1. 发出sendfile()系统调用,处理器从用户空间切换至内核空间;

  2. 通过DMA将数据copy至内核空间缓冲区;

  3. 将数据在内核空间缓冲区的地址和偏移量拷贝至目标socket的缓冲区;

  4. Sendfile()返回,处理器从内核空间切换至用户空间。

  5. 带有scatter/gather 功能的DMA将数据直接从内核缓冲区读取到协议引擎,从而消除了最后一次CPU拷贝。

总结一下,这种方法产生了2次上下文切换和2次数据拷贝。

这时有人可能会问,如果我把数据从磁盘上读出来后,再编辑一下,再发送出去,以上所说的零拷贝岂不是不能实现?
对于该问题,Linux提供了mmap来实现。

通过mmap实现的零拷贝

mmap(内存映射):mmap操作提供了一种机制,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。

Linux下的零拷贝

  1. 发出mmap()系统调用,处理器从用户空间切换至内核空间。

  2. 向磁盘请求数据;

  3. 通过DMA将数据从磁盘拷贝至内核空间缓冲区;

  4. mmap()调用返回,这时候用户程序和操作系统共享这个缓冲区,不需要再将数据从kernel buffer 拷贝至 user buffer,处理器从内核空间切换至用户空间;

  5. 用户逻辑处理;

  6. 发出write()系统调用,将数据从内核空间缓冲区拷贝至目标socket缓冲区,这时处理器从用户空间切换至内核空间;

  7. write()调用返回,处理器从内核空间切换至用户空间;

  8. 通过DMA将数据拷贝至协议引擎。

总结一下:这种方法将产生4次上下文切换和3次数据拷贝。

至此,零拷贝技术就介绍完了。本文所提及的零拷贝技术都是需要底层操作系统支持的,同时,零拷贝技术一直是在不断地发展和完善当中的,本文并没有涵盖 Linux 上出现的所有零拷贝技术。

另外本文是通过查阅资料及视频学习后对零拷贝技术的一个粗浅的认识,如果有不正确的地方,欢迎指出~

参考

Zero Copy I: User-Mode Perspective
Linux 中的零拷贝技术,第 1 部分
Linux 中的零拷贝技术,第 2 部分
圣思园《并发与Netty》

相关推荐