零拷贝

为什么要零拷贝

传统的 IO 拷贝在计算机中拷贝次数太多,速度太慢,零拷贝可以减少拷贝次数,增加系统性能。另外,零拷贝并不是指没有进行文件的拷贝,只是减少了拷贝的次数。

DMA

直接内存访问(Direct Memory Access)是一种执行 I/O 的工作方式。在这种方式中,DMA 控制器从 CPU 完全接管对总线的控制。这意味着数据交换不经过 CPU,而直接在内存和 I/O 设备之间进行 。DMA 方式一般用于高速传送成组数据。DMA 控制器将向内存发出地址和控制信号,修改地址,对传送的字的个数计数,并且以中断方式向 CPU 报告传送操作的结束。

DMA 方式的主要优点是速度快。由于 CPU 根本不参加传送操作,因此就省去了 CPU 取指令、取数、送数等操作。在数据传送过程中,没有保存现场、恢复现场之类的工作。内存地址修改、传送字个数的计数等等,也不是由软件实现,而是用硬件线路直接实现的。所以 DMA 方式能满足高速 I/O 设备的要求,也有利于 CPU 效率的发挥。

用户态和内核态

在 Linux 系统中,有的程序权限很高,可以访问计算机的任何资源,但是有的程序权限就低,只能访问部分资源。这两个类型的程序,就可以映射为用户态和内核态。内核态是计算机的核心,可以访问计算机的任何资源,如网卡、硬盘。为了安全,CPU 不能让用户程序肆无忌惮的访问计算机的任何资源,这样如果用户程序不稳定可能会造成系统崩溃,因此才有的用户态。

  • 内核态:可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu 也可以将自己从一个程序切换到另一个程序。
  • 用户态:只能受限的访问内存,且不允许访问外围设备,占用 cpu 的能力被剥夺,cpu 资源可以被其他程序获取。

综上所述,在 CPU 想要读取硬盘文件的时候,需要从用户态切换为内核态,才有权限。读取完成之后,为了程序安全,需要从内核态切换为用户态。

普通拷贝

1
1-1

在普通的拷贝时,流程如下:

  1. 切换到内核态,先到内核态查询内核缓冲区,如果内核缓冲区有数据,则可以直接拷贝到用户空间中。如果内核缓冲区没有,则 CPU 会让 DMA 加载到内核空间中。这里就会有一次 DMA 拷贝。
  2. 拷贝到内核缓冲区之后,CPU 将会从内核缓冲区拷贝走。这是一次 CPU 拷贝。拷贝完成切换到用户态。
  3. 写数据的时候,再次切换到内核态。切换完成之后,写到 socket 缓冲区。写完之后,切换到用户态。
  4. DMA 通过异步的方式将 socket 缓冲区的数据通过网卡发送到对端。

这种普通的IO。总共有 4 次 CPU 切换(上图蓝色)。分别是:读 2 次、写 2 次。

4 次文件拷贝,分别是:

  1. 文件从硬盘到内核空间
  2. 内核空间到 CPU
  3. CPU 到 socket 缓冲区
  4. socket 缓冲区到网卡。

零拷贝

mmap

2
mmap 是零拷贝的一种方式通过虚拟内存的方式实现。也就是说用户空间和内核空间使用同一个物理地址。这样,文件就不在需要经过用户空间。可以从内核缓冲区直接复制到 socket 缓冲区。减少了一次文件拷贝。流程如下:

  1. 调用 mmap,将一块用户空间映射到内核空间。此时进入内核态。DMA 把数据加载到内核空间,这是一次 DMA 拷贝。
  2. 切换回用户态。
  3. 调用 write,向用户空间写数据。数据直接从内核缓冲区写入到 socket 缓冲区。这是一次 CPU 拷贝。
  4. 返回用户态。DMA 异步的将 socket 缓冲区内容通过网卡发送到对端。

共计 3 次内存拷贝,4 次用户内核态切换。

sendfile

3
3-1
sendfile 函数可以在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝。流程如下:

  1. 系统调用 sendfile() 通过 DMA 把硬盘数据拷贝到内核缓冲区,这是一次 DMA 拷贝。
  2. 然后数据被内核拷贝到另外一个与 socket 相关的 socket 缓冲区。这里没有用户态和核心态之间的切换,在内核中直接完成了从一个缓冲区到另一个缓冲区的拷贝。这里虽说是写了 cpu 复制,但是如果网卡支持 scatter-gather ,并没有直接复制内容,而是复制了一些 offset 和 length 之类的数据到 socket 缓冲区。如下图:
    3-2
  3. DMA 异步将 socket 缓冲区内容发送给对端,是一次 DMA 拷贝。如果网卡支持 scatter-gather,那么就直接从内核缓冲区直接 DMA 拷贝到网卡驱动。

共计 3 次或 2 次内存拷贝,2 次用户内核态切换。