有关Linux下的零拷贝技术


概述

零拷贝技术是一种IO操作优化技术。可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间。

一. 相关概念

用户进行IO操作,其实也就是应用程序访问系统资源,即通过系统调用 或者中断(外中断、内中断)从而使得 CPU 从用户态转向内核态。

系统调用其实就是一些函数,用于对文件和设备进行访问和控制。最常见的有两种:

  • read:从文件中读取内容。
  • write:往文件中写入内容。

1.1 缓冲区

在复习IO模型相关的知识的时候,就遇到这么两个概念:

  • 内核缓冲区。
  • 用户缓冲区。

这个到底有什么区别呢?我们知道,我们的应用程序从磁盘上读取数据的时候,一般都是分成两步:

  1. 操作系统(内核)从磁盘上读取数据存到内核空间。
  2. 再把数据从内核空间拷贝到用户空间。

那么这个过程中就会涉及到两次数据读操作:

  1. 磁盘上读取。
  2. 内存中读取。

访问磁盘的速度要远远小于访问内存的速度,那么整个读取数据的操作中耗费时长最长的阶段自然在磁盘读取上。因此出现了内核缓冲区以及对应的用户缓冲区。至于具体流程可以看另外几篇博文。

1.1.1 内核缓冲区

内核缓冲区,其实可以从两个方向去理解:

  • 缓冲Buffer
  • 缓存Cache

它的作用如下:

  • 数据预读(缓存功能):当程序发起read系统调用的时候,内核会读更多磁盘上的数据,以备程序后续使用。(假设我的read请求可能只需要读100KB的数据,那么此时内核会读200KB,就是这个意思。)
  • 延时回写(缓冲功能):当程序发起 write 系统调用时,内核并不会直接把数据写入到磁盘文件中,而是写入到缓冲区中。当缓冲区中的数据积累到一定程度,才将数据真正地刷新到磁盘中。

1.1.2 用户缓冲区

用户缓冲区的作用和内核缓冲区一样,都是数据的预读以及延时回写。不过两者出现的目的还是不一样:

  • 内核缓冲区:主要处理的是内核空间和磁盘之间的数据传递,目的是减少访问磁盘的次数。
  • 用户缓冲区:主要处理的是用户空间和内核空间之间的数据传递,目的是减少系统调用的次数。

1.2 DMA技术

DMA的全称:Direct Memory Access,直接存储器访问。DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。

为什么要有DMA技术?我们来看下IO操作的前后流程对比:

总结就是:DMA帮助CPU将数据从磁盘拷贝至内核缓冲区中,让CPU解放双手。

1.3 虚拟内存

虚拟内存:即拿出一部分硬盘空间来充当内存使用,当内存占用完时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。一般虚拟内存都用来替代一部分物理内存的,有这么几个好处:

  1. 多个虚拟内存可以指向同一个物理地址(多对一)。
  2. 虚拟内存空间可以远远大于物理内存空间(空间大)。

二. 零拷贝

2.1 传统文件传输流程

我们用一个最基本的文件读取操作来看下流程,无非分为两个步骤:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);
  1. 将磁盘上的文件读取出来。
  2. 将文件数据通过网络协议发送给客户端。

流程图如下:

这里能得到几个信息:

  1. 期间完成了4次数据拷贝。
  2. 完成了4次的用户态和内核态之间的状态切换,我们简称上下文切换。

备注:每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。因此相当于2次上下文切换。问题就是:再这样的传统IO传输模型中,用户为了获取服务器上的某个数据,期间竟然有4次数据的搬运过程,而过多的数据拷贝会消耗CPU资源,在高并发的情况下更是大大降低了系统的性能。

如何减少用户态和内核态之间的上下文切换以及内存拷贝的次数” 成了提高文件传输性能的一个关键点。

2.2 零拷贝技术原理

还记得上文提到的虚拟内存的概念吗?零拷贝技术中就用到了虚拟内存可以多对一的一个特性:将内核空间和用户空间的虚拟地址映射到同一个物理地址,这样在 I/O 操作时就不需要来回复制了。

零拷贝实现的方式有多种:

  • mmap + write
  • sendfile
  • sendfile+DMA收集
  • splice

2.2.1 mmap+ write

mmap()函数也是属于系统函数的一种,在 2.1 节当中,我们知道read()函数会把内核缓冲区的数据拷贝到用户的缓冲区里。mmap()函数就可以减少这一步开销,因为它会直接把内核缓冲区里的数据映射到用户空间。那么这种模式下,文件传输模型图就会有所改变:

具体过程如下:

  • 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
  • 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
  • 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。

可以看到:

  1. 虽然说拷贝过程从原来的4次—->3次。
  2. 但是系统调用依旧是2次。原本:read + write。现在:mmap + write

2.2.2 sendfile&&sendfile+DMA收集

sendfile()是一个专门发送文件的系统调用函数,由于 sendfile 仅仅对应一次系统调用,而传统文件操作则需要使用 read 以及 write 两个系统调用。

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • out_fd:目的端的文件描述符。
  • in_fd:源端文件描述符。
  • offset:源端数据偏移量。
  • count:复制数据的长度。

最终的返回是实际复制数据的长度。该函数的作用主要有两点:

  1. 可以同时替代read()和write()函数。即可以减少一次系统调用。
  2. 通过SG-DMA控制器减少数据拷贝的次数。

这个SG的前缀是什么意思呢?

SG:scatter-gather。其原理就是在内核空间 Read BufferSocket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer,这样就不需要复制。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。

流程如下:

  1. 通过 DMA 将磁盘上的数据拷贝到内核缓冲区里。
  2. 将缓冲区描述符和数据长度传到 socket 缓冲区,SG-DMA控制器直接将内核缓存中的数据拷贝到网卡的缓冲区里, 因此不需要将内核缓冲区的数据拷贝到socket缓冲区中。

如图:

这就是零拷贝技术。即没有再内存层面去拷贝数据,整个过程中CPU都没有参与数据的拷贝,都是交给DMA来完成。

相对于传统的文件传输方式,零拷贝的优势:

  1. 减少了2次上下文切换和数据拷贝。
  2. 2次的数据拷贝过程中,无需CPU参与,因此CPU可以在此期间做其他事情。

注意:零拷贝并不是说数据传输过程中拷贝的次数为0,而是指不存在内存层面的数据拷贝(一共2次)。因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

2.2.3 splice方式

splice系统调用是Linux 在 2.6 版本引入的,其不需要硬件支持,并且不再限定于socket上,实现两个普通文件之间的数据零拷贝。

splice 调用和sendfile 非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

总结

无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。

下图展示了各种零拷贝技术的对比图:

CPU拷贝DMA拷贝系统调用上下文切换
传统方法22read/write4
内存映射12mmap/write4
sendfile12sendfile2
scatter/gather copy02sendfile2
splice02splice0

文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录