零拷贝
Zane Lv4

前言


在之前的Blog中,浅浅讲解了一下DMA子系统在Linux以及SylixOS中的简单应用,在这个基础上又接触到了熟悉但不了解的新概念—–零拷贝(Zero-copy)。

为什么需要零拷贝(Zero-copy)?

虽说目前市面上的SSD硬盘已经有非常快的数据传输速度,常见的接口例如SATA、mSATA、PCIe、M.2、U.2都拥有比较成熟的技术积累。

像是M.2接口的理论速度已经可以达到32Gbps,尽管如此,在整个计算机系统当中,硬盘的速度依旧是最慢的硬件之一,就拿和内存相比读写速度可能相差10倍以上,更别说挨着CPU的一些缓存了。

所以零拷贝是一种针对优化磁盘的技术,其目的就是为了提高系统的吞吐量同时减少操作系统内核对磁盘的访问次数。

什么是零拷贝?

字面意思理解就是“没有拷贝”,即在计算机执行I/O操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而减少上下文切换以及CPU的拷贝时间。

插一句,除了零拷贝以外还有很多技术被用于此,比如异步I/O、高级I/O等(挖坑我最在行)

普通I/O

较早时期的I/O处理过程很容易理解:

  1. CPU丢出请求指令给磁盘控制器,然后返回;

  2. 磁盘控制器收到数据后,开始准备数据,将数据放入到磁盘控制器的内部缓存区,随后产生中断

  3. CPU接收到中断信号以后,停下原先的任务,上下文保存,将磁盘控制器的缓冲区的数据按特定的模式读入自身的寄存器,然后将数据写入内存。

I/O操作的痛点就在CPU一直因为需要数据“这点儿小事“忙的不可开交,在数据请求以及数据传输的阶段CPU是没有执行其他任务的,这就导致了CPU的利用率受到影响。

如下图:

对于简单的数据CPU的速度还是绰绰有余的,但是如今存储设备以及网络设备的速度也同样很快,如果还使用这种技术进行I/O操作,CPU就得专门拿出来一个核用来当“搬运工”。

这也是为什么有了DMA技术,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务

DMA的加入

加入DMA控制器后的I/O请求如下:

具体过程:

  1. 用户进程调用read方法,像操作系统发出I/O请求,请求读取数据到内存缓冲区中,进程进入阻塞状态。

  2. 操作系统收到请求后,将I/O请求送给DMA,这时CPU可以执行其他任务。

  3. DMA进一步将I/O请求发送给磁盘。

  4. 磁盘收到DMA的I/O请求后,将数据从磁盘读取到磁盘控制器的缓冲区中,当缓冲区读满,向DMA发送中断信号,告知缓冲区已满。

  5. DMA收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区,此时仍然不需要占用CPU。

  6. 当DMA读取了足够多的数据,就会发送中断信号给CPU。

  7. CPU收到DMA信号后,才会再次放下手中的工作,将数据从内核拷贝到用户空间,系统调用返回。

这样,CPU就从将磁盘控制器的缓冲区的数据按特定的模式读入自身的寄存器,然后将数据写入内存。这块工作中解脱出来,而是由DMA完成。

早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。

优化数据传输性能的方法

从刚才的流程图中可以看出,通过以下两种方法进行性能提升。

  1. 减少用户态和内核态的上下文切换

  2. 减少数据拷贝的次数

减少用户态与内核态的上下文切换

当读取磁盘数据时,由于用户空间的权限问题不能直接读取磁盘和网卡信息,只能拜托内核空间进行相关操作,而进行一次两核模式的转换就需要一次系统调用,而一次系统调用势必会发生两次上下文切换

从用户态到内核态,等内核空间执行完任务,再切换回用户态有进程代码执行。

简单的理解就是减少系统调用的次数,便可以减少上下文切换次数。

减少数据拷贝的次数

在普通I/O中一共进行了4次数据拷贝,从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里

在文件传输时,用户空间并不会对数据进行改造,所以数据并不需要搬进用户空间,因此用户空间的缓冲区是可以在过程中省略的

实现零拷贝

  • mmap + write

  • sendfile

mmap + write

read系统调用时会把内存缓冲区的数据拷贝到用户缓冲区中,可以通过使用mmap替换read的方式减少掉这一步开销。

C
1
2
buf = mmap(file, len);
write(sockfd, buf, len);

mmap() 系统调用函数会直接把内核缓冲区里的数据【映射】到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

  1. 进程调用mmap后,DMA把磁盘中的数据拷贝到内核的缓冲区内。随后,应用进程跟操作系统共享这个缓冲区。

  2. 应用进程再调用write,操作系统直接将内核缓冲区的数据拷贝到socket缓冲区中,这个过程发生在内核态,由CPU搬运数据。

  3. 将内核的socket缓冲区里的数据,拷贝到设备(如网卡)的缓冲区里,这个过程由DMA搬运。

但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

sendfile

在Linux内核2.1中,sendfile为系统调用函数专门用来发送文件。

C
1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

SG-DMA(The Scatter-Gather Direct Memory Access)

通过这个技术,进一步减少CPU把内核缓冲区里的数据拷贝到socket缓冲区的过程。

从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:

  • 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
  • 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;

所以,这个过程之中,只进行了 2 次数据拷贝,如下图:

因为没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

总结

零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。

参考

9.1 什么是零拷贝? | 小林coding (xiaolincoding.com)

TO-DO

  • PageCache

  • 大文件传输

  • 异步I/O