零拷贝

1. 什么是零拷贝

零拷贝是一种高效的数据传输技术,可以将数据从内核空间直接传输到应用程序的内存空间中,从而大量地节省CPU时间

传统的数据传输,数据需要从磁盘读取到内核空间,再将数据从内核空间拷贝到用户空间,再从用户空间拷贝到应用程序的内存中,这几次拷贝其实都会耗费大量的CPU资源和内存空间

所以是不是可以有一种方式能够直接从内核空间拷贝到应用空间呢,并且不占用CPU的资源(至少可以不需要从内核态切换到用户态,再从用户态切换到内核态,上下文的切换其实很耗性能),因此诞生了零拷贝

2. 什么是CPU上下文切换?

实际上操作系统是一个多任务操作系统,支持远大于CPU数量的任务同时运行着,而CPU会给每个任务分配运行时间,来达到似乎每个任务似乎都在同时并行的错觉,实际上是因为操作系统会在很短的时间内将CPU时间轮流分配给这些任务,造成这种操作

CPU上下文切换是指从一个正在执行的进程或线程切换到另一个进程或线程时,操作系统需要保存当前正在执行进程(或线程)的状态信息,以便稍后能够恢复到该状态,同时加载新进程(或线程)的状态,使其能够继续执行

上下文切换一般发生在多任务操作系统,本质上指的是多个进程和线程共享同一个CPU资源,当操作系统决定需要切换到另一个线程或进程时,会需要执行以下的步骤:

  • 保存当前上下文:操作系统需要将当前进程 / 线程的所有完整状态信息做保存操作,如寄存器值、程序计数器、堆栈指针等等,将这类部件的数据记录至内存或内核数据结构中
  • 加载新的上下文:操作系统会从内存或内核数据结构中恢复新进程 / 线程的状态信息,将其加载到CPU的寄存器和其他相关的硬件部件中
  • 继续执行:当新的进程 / 线程上下文加载完成后,CPU则会开始执行该进程 / 线程,且会从上次暂停的地方开始执行

CPU上下文切换这个行为是由操作系统内核管理的,允许有多个进程或线程来共享CPU,来实现多任务处理,但是上下文切换是有开销的,因为涉及到了保存和恢复大量的状态信息,这个步骤消耗了一定的CPU时间和内存带宽

3. 什么是内核态 / 用户态?

Tips:内核态通常也被称之为内核空间,用户态通常被称之为用户空间

通过系统调用将Linux分为内核态和用户态,可以把它俩简单地理解成是不同的权限空间

内核态下,掌控了所有操作系统的特权,能够调用硬件资源,控制CPU的资源与分配内存资源,再简单地理解,实际上在内核态下就相当于是操作系统的超级管理员,能够掌控一切系统底层调用接口

而在用户态下,则对应的只能够通过调用内核提供的通用访问接口,来达到操作内存,调度CPU和I/O等特权,一般是应用程序运行的空间

区分这两个空间的目的是为了防止应用程序通过特权肆意地占用操作系统的资源,为了保护操作系统的资源合理性,衍生出了高特权的内核态和只能调用通用接口达到目的的用户态

这两者说白了就是权限的不同

4. 用户态切换到内核态属于CPU上下文切换吗?

属于

5. 为什么需要零拷贝?

传统的数据文件传输如下所示:

image-20230831014837186

可以观察到,传统的文件传输方式会产生大量的上下文切换,而上下文切换会导致不必要的CPU消耗与内存空间的占用,因此需要零拷贝技术来减少性能浪费,整体步骤如下:

  • 用户进程发起read()系统调用,触发上下文切换,由用户态切换至内核态
  • 进入内核态后,由CPU发起IO请求,通过直接内存访问 [Direct Memory Access - DMA] 的方式从磁盘中读取数据并拷贝到内核缓冲区中
  • 将内核缓冲区的数据拷贝至用户态的用户空间缓冲区中,并触发上下文切换,从内核态切换至用户态
  • 用户进程发起write()系统调用,触发上下文切换,从用户态切换至内核态
  • 进入内核态后,将数据从用户空间缓冲区拷贝至内核空间中的目标Socket关联的缓冲区
  • 数据最终从Socket通过DMA传送到网卡缓冲区,write()系统调用完成并返回,同时由内核态切换到用户态

6. 什么是DMA?

在没有DMA之前,所有的数据从磁盘提取到缓冲区都需要CPU停下它们手动的工作,来进行数据的提取,数据的传输期间,CPU是无法进行其他操作的,因此,为了提升整体的效率,发明了DMA技术,全称是 DIrect Memory Access

简单的理解DMA,就是在I/O设备和内存的数据传输时,数据的搬运工作都交给了DMA控制器,CPU则不会再参与数据搬运的工作,这样CPU可以在数据在传输的过程中进行其他的任务

7. 零拷贝的原理?

零拷贝技术可以由sendfilemmapDirect I/Osplice等几种方式实现

a. sendfile()

sendfile()系统调用是在两个文件描述符之间直接传递数据(所有操作均在内核空间中操作),从而避免了数据在内核缓冲区和用户缓冲区之间的拷贝,操作效率很高

其本质是利用了DMA引擎将文件中的数据拷贝到操作系统的内核缓冲区中,然后数据被拷贝到Socket相关的协议引擎中

因为sendfile()系统调用不需要将数据拷贝或者映射到应用程序地址空间中去,因此sendfile()的一个问题也在与它只适用于应用程序地址空间不需要对所访问的数据进行处理的情况,而得益于其传输的数据没有越过用户应用程序 - 操作系统内核的边界线,因此sendfile()减少了大量的存储管理开销,整体如下所示:

image-20230901010242618

整体步骤如下:

  • 由用户应用程序进程发起sendfile()调用,此时发生上下文切换,由用户态切换至内核态
  • DMA控制器将数据从磁盘拷贝至内核缓冲区中
  • CPU将内核缓冲区中相关文件的文件描述符信息(包含了内核缓冲区的内存地址以及偏移量)发送至Socket缓冲区
  • DMA控制器根据文件描述符信息直接将文件数据从内核缓冲区拷贝至网卡
  • 完成后结束sendfile()调用,同时发生再一次的上下文切换,由内核态切换至用户态

Tips:

只有网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术才可以通过传递文件描述符的方式避免内核空间内的一次 CPU 拷贝

这意味着此优化取决于 Linux 系统的物理网卡是否支持(Linux 在内核 2.4 版本里引入了 DMA 的 scatter/gather – 分散 / 收集功能,只要确保 Linux 版本高于 2.4 即可)

b. mmap()

mmap()是内核提供的系统调用函数,能够将磁盘数据读入内核缓冲区后,将内核缓冲区对应数据地址映射至用户缓冲区,使其整体减少一次CPU拷贝,但内核空间仍然有1次CPU拷贝,同时依然会有4次CPU上下文切换,整体如下所示:

image-20230902180220053

c.Direct I/O

Direct I/O为直接IO,零拷贝的其他实现技术中,都需要将数据缓存在内核空间的缓冲区中,数据至少都需要再内核缓冲区中存储一份,但是在Direct I/O技术中,数据直接存储在了用户空间中,绕过了内核,用户空间直接通过DMA的方式与磁盘以及网卡实现数据拷贝,如下所示:

img

d. splice()

splice()是内核空间提供的调用方法,相当于在内核缓冲区与Socket缓冲区之间开辟了一个pipe通道用于文件传输,基于splice()调用,整个拷贝过程会发生2次上下文切换,0次CPU拷贝以及2次DAM拷贝

其主要的优势在于减少了数据拷贝的次数和数据在用户空间和内核空间之间的来回传输,从而显著地提高了数据传输的效率,

如下所示:

image-20230902191757761

8. 什么是文件描述符?

也称为 File Descriptor,可以简单地理解成是计算机底层索引文件的一个符号引用,其本质是一个非负整数,当程序打开或者创建一个文件时,内核向进程返回的是一个文件描述符

同时,为了防止应用程序资源滥用的情况,每个进程能够获取的文件描述符也是有限的,但能够通过系统调用接口来获取更多的文件描述符