零拷贝技术

零拷贝是指计算机执行操作时,不需要cpu将数据重一个内存区域拷贝到另外一个内存区域,从而减少上下文切换个cpu的拷贝时间。它的作用是减少网络设备到用户程序空间数据传递过程中的数据拷贝次数、系统调用,实现cpu的零参与,彻底消除cpu在IO过程中的负载。实现零拷贝的技术主要有DMA数据传输技术和内存映射技术。

  • 零拷贝可以减上内核缓冲区和用户进程缓冲区的反复拷贝
  • 零拷贝可以减少用户进程地址空间和内核进程地址空间因上下文切换带来的cpu 的额外开销

用户空间和内核空间

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space)。 在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态。

内核空间

内核空间总是在内存中,它是为操作系统内核准备的。应用程序不允许直接在内核空间进行读写和其他直接调用函数的方式进行操作

用户空间

每个用户都有自己的用户空间,处于用户态的进程不允许直接操作内核空间中的数据,所以系统调用的时候需要切换到内核态才可以,用户空间包括:

  • 运行时栈
  • 运行时堆
  • 代码段
  • 未初始化的数据段
  • 已初始化的数据段
  • 内存映射区域

Linux I/O读写

linux提供了轮询、I/O中断、和DMA传输三种方式进行数据传输。

  • 其中轮询是基于对I/O端口进行不断的检测;
  • I/O中断是当数据到达磁盘缓冲区时,对cpu发起中断,让cpu完成接下来的传输;
  • DMA传输在I/O中断的基础上插入DMA磁盘控制器,由DMA负责传输数据,降低cpu的负载。

I/O中断

在DMA出现之前,每次用户读取磁盘数据时,都需要I/O中断,每次I/O中断都会引起cpu上下稳的切换

image

DMA传输原理

DMA 的全称叫直接内存存取(Direct Memory Access),是一种允许外围设备(硬件子系统)直接访问系统主内存的机制。在整个数据传输过程中cpu只在开始和结束的时候做一点工作,而在传输过程中可以去做额外的事情,在大部分时间里,cpu和I/O处于并行的状态,大大提升计算机系统的性能。

image

传统的I/O模式

传统的I/O模式,是用户进程通过read()函数先将数据拷贝至用户缓冲区,然后通过write()函数将缓冲区的传输至网络端口。

整个数据读写流程涉及,
2次cpu拷贝(1次读过程的内核缓冲区至用户缓冲区,1次写过程的用户缓冲区至socket缓冲区),
2次DMA拷贝(1次读过程的数据磁盘缓冲区至内核缓冲区,1次socket缓冲区至网卡),
4次上下文切换(read和write函数各两次的用户态和内核态的来回切换)

零拷贝方式

  • 用户态直接 I/O:应用进程直接访问硬件存储,操作系统只是辅助操作数据传输。这种方式依然会存在用户空间和内核空间的上下文切换,但是数据直接拷贝至用户空间,直接I/O减少了一次内核空间拷贝至用户空间操作
  • 减少数据拷贝次数:在数据传输过程中,避免用户缓冲区和内核缓冲区之间的数据拷贝,以及数据在系统内核空间的数据拷贝;这也是当下主流的零拷贝实现方式
  • 写时复制:写时复制是数据在多进程共享一份数据时,将数据拷贝至自己的用户空间,防止其他进程对数据修改的一种手段

用户态直接 I/O

这种方式直接跨过内核空间,允许用户进程从硬件设备直接获取数据,绕过socket缓冲区,大大的提升了性能

image

用户态I/O只适合不需要内核缓冲区的应用程序,这些程序通常有自己的进程地址空间有自己的缓存机制,如数据库管理系统就是这样的。

减少数据拷贝次数

mmap + write

这种方式是用mmap+write代替原来的read+write模式,减少一次cpu拷贝,mmap是linux系统的文件映射方法;使用mmp的目的是减少内核缓冲区至用户缓冲区的过程。

image

  1. 用户进程向内核通过mmap()函数发起调用,上下文从用户态转为内核态
  2. 将用户缓冲区和内核读缓冲区进行映射
  3. cpu利用DMA控制器将数据从磁盘拷贝至内核读缓冲区
  4. 上下文切换为用户态,mmap()函数执行返回
  5. 用户进程发起write()请求,由用户态转为内核态
  6. cpu将数据从内核缓冲区拷贝至网络缓冲区
  7. cpu利用DMA控制器将数据拷贝至网卡
  8. 上下文切换回用户态,write()函数执行返回

mmap+write模式整个过程包括4次上下文切换,1次cpu拷贝,2次DMA拷贝

从上面流程看到mmap+write模式能减少一次拷贝,提升了I/O性能;但是这种方式只适合大文件,对于小文件,反而会产生大量的内存碎片,因为内存映射总是要对齐页边界,最小单位是 4 KB,一个 5 KB 的文件将会映射占用 8 KB 内存,也就会浪费 3 KB 内存。

sendfile

sendfile在 linux2.1 时被引入,目的是减少数据传输过程,通过 sendfile 可以直接在内核空间直接进行I/O传输,从而省去内核空间和用户空间的上下文切换,sendfile的I/O过程对用户完全不可见,更符合我们对文件传输的理解

image

  1. 用户进程通过sendfile()发起文件传输请求,由用户态转为内核态
  2. cpu利用DMA磁盘控制器将数据从磁盘拷贝至内核缓冲区
  3. cpu将数据从内核缓冲区拷贝至socket缓冲区
  4. cpu利用DMA控制器将数据从socket缓冲区拷贝至网卡
  5. 内核态转为用户态,执行sendfile()函数返回

基于sendfile的零拷贝,整个过程发生2次上下文切换,一次cpu拷贝,2次DMA拷贝

相较于mmap方式,sendfile减少了两次的上下文切换,但是引入另外一个问题,就是用户进程无法对数据进行修改,只能单纯完成一次数据传输。

sendfile + DMA gather copy

linux 2.4 对sendfile进行了修改,引入了 DMA gather copy,将内核缓冲区中的文件描述符记录到了网络缓冲区(内存地址、地址偏移),DMA根据内存地址和地址偏移量将数据拷贝至网卡,减上一次cpu拷贝,这本质上和mmap的原理想类似

image

  1. 用户进程通过sendfile()函数发起系统调用,由用户态转为内核态
  2. cpu利用DMA控制器将数据从磁盘拷贝至内核缓冲区
  3. cpu向socket缓冲区发送文件描述符和文件长度等信息
  4. cpu利用DMA控制器基于发送的文件描述符等信息,直接从内核缓冲区拷贝至网卡
  5. 内核态转为用户态,sendfile执行返回

在sendfile + DMA gather copy 模式中,共发生两次上下文切换,两次DMA拷贝,0次cpu拷贝

sendfile + DMA gather copy 模式同样存在用户进程无法修改数据的问题,而且本身也需要硬件的支持,它只适用于将数据从文件拷贝到 socket 套接字上的传输过程

splice

由于sendfile需要硬件设备的支持,在 linux2.6.17 版本引入了splice系统调用,不仅不需要硬件的支持,还实现了文件描述符中的零拷贝。splice在内核缓冲区与socket缓冲区之间建立管道(pipeline),避免两者之间的cpu拷贝。

image

  1. 用户进程通过splice()发起系统调用,有用户态转为内核态
  2. cpu利用DMA控制器将数据从磁盘拷贝至内核缓冲区
  3. cpu在内核缓冲区和socket缓冲区之间建立通道
  4. cpu利用DMA控制器将数据从网络缓冲区拷贝至网卡
  5. 由内核态转为用户态,splice()函数返回

基于splice零拷贝模式,整个过程会发生2次上下文切换,0次cpu拷贝和2次DMA拷贝

splice 拷贝方式也同样存在用户程序不能对数据进行修改的问题。除此之外,它使用了 Linux 的管道缓冲机制,可以用于任意两个文件描述符中传输数据,但是它的两个文件描述符参数中有一个必须是管道设备。

对比

拷贝方式 CPU拷贝 DMA拷贝 上下文切换
传统方式 2 2 4
mmap+write 1 2 4
sendfile 1 2 2
sendfile+DMA gather copy 0 2 2
splice 0 2 2