零拷贝技术的使用
概念
零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
目标
在应用程序与操作系统中使用零拷贝技术并非是没有拷贝,而是减少CPU拷贝的次数,达到提升应用程序的执行效率。
在了解零拷贝技术以及概念之前,需先了解一下CPU上下文切换、DMA以及Buffer/Cache。
CPU上下文切换
简介
上下文切换(context-switch)指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。上下文切换过程中的信息被保存在进程控制块(PCB-Process Control Block)中。PCB又被称作切换帧(SwitchFrame)。上下文切换的信息会一直被保存在CPU的内存中,直到被再次使用。
在系统中上下文切换总共分为如下:
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
进程上下文切换
进程内部上下文切换
出于安全考虑,将CPU的运行等级分为4种:Ring0、Ring1、Ring2、Ring3,运行等级依次降低,操作系统内部指令只会在Ring0级别的CPU上运行,而第三方任意应用程序均是在Ring3级别上运行。在Linux和Windows系统上只采用了Ring0以及Ring3级别,也称为内核空间(Ring0)以及用户空间(Ring3)。
运行等级如下:
当CPU运行级别从内核态切换成用户态或者从用户态切换成内核态时,发生在进程内部的上下文切换;CPU从用户态切换到内核态时,需要做的操作如下:
- CPU寄存器会将当前进程用户态所在的指令位置保存起来。
- 切换CPU运行等级
- 更新进程内核态指令位置
- 执行内核态任务
这就是一次CPU进程上下文切换,如果从内核态在切换到用户态时,需要再做一次上面的操作。
在进程内部中用户态与内核态之间的切换,也叫特权模式切换;对于发起这种用户态与内核态之间的切换操作称之为系统调用;比如程序需要read、write、open以及close等对硬件(包括磁盘或者网卡)发起的操作都属于系统调用。
对于CPU的运行等级,则是保存在CPU的段寄存器当中,确切的说是段寄存器中的cs(Code Segment),进行用户态与内核态之间的切换,则是更改cs段寄存器中的RPL的运行等级标识位。
CPU寄存器大致结构如下:
进程之间上下文切换
进程由内核管理和调度,进程的切换只能发生在内核态,进程上下文不仅包括进程的虚拟内存、栈、全局变量等用户空间资源,还包括内核堆栈、寄存器等内核空间状态。每次进程上下文切换需要几十纳秒到数微秒的CPU时间。
不同进程之间发生上下文切换过程:
- 首先将进程的虚拟内存、堆栈、全局变量信息等等保存,在保存进程内核态和CPU寄存器信息
- 加载下一个进程的虚拟内存、堆栈、全局变量信息、内核态信息
- 刷新进程的虚拟内存和用户栈信息
- 开始执行下一个进程
并且Linux通过TLB来管理虚拟内存到物理内存之间的映射,当虚拟内存更新后,TLB也需要刷新,内存的访问也会随之变慢。特别在多处理器系统上,缓存被多个处理器共享,刷新缓存不仅会影响当前处理器的进程,还会影响共享缓存的其他处理器的进程。
发起进程上下文切换时机
在进程调度的时候,需要进行切换上下文。Linux为每个CPU维护一个就绪队列,将活跃进程(正在运行和正在等待CPU的进程)按照优先级和等待CPU的时间来排序,然后选择最需要CPU的进程运行。(优先级高和等待时间长的进程)
发生了以下五点是会出现进程上下文切换:
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
线程是处理器任务调度和执行的基本单位,进程是操作系统资源分配的基本单位。内核中的任务调度,实际上的调度对象是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这么理解:
- 当进程只有一个线程时,可以认为进程就等于线程。
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
- 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的.
那么线程的上下分可以分为
- 进程内线程的上下文切换
- 进程间线程的上下文切换
理解为:进程内的线程上下文切换吗,会有共享的资源不需要保存和重新加载,比进程间的线程上下文切换消耗更少的资源。合理的在进程内使用多线程可以提高效率和负载。
中断上下文切换
操作系统将中断分为硬中断以及软中断。
硬中断
由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。因此是由外部事件引起的因此具有随机性和突发性。
软中断
为了满足实时系统的要求,中断处理应该是越快越好。Linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。
例如:系统调用就是通过软中断进行实现的。
DMA
简介
直接内存访问(DMA,Direct Memory Access)是一些计算机总线架构提供的功能,它能使数据从附加设备(如磁盘驱动器)直接发送到计算机主板的内存上。
允许不同速度的硬件设备来沟通,而不需要依于中央处理器的大量中断负载。否则,中央处理器需要从来源把每一片段的数据复制到寄存器,然后把它们再次写回到新的地方。在这个时间中,中央处理器对于其他的工作来说就无法使用。
DMAC
DMA控制器,本质是上位于主板上的一个独立的芯片,主要工作是用于协助CPU完成一些数据传输工作,称之为协处理器,即Co-Processor。
使用DMA进行传输数据的过程如下:
- 首先,CPU 还是作为一个主设备,向 DMAC 设备发起请求。这个请求,其实就是在DMAC 里面修改配置寄存器。
- CPU修改DMAC的配置的时候,会告诉DMAC这样几个信息
- 首先是源地址的初始值和传输时候的地址增减方式
- 所谓源地址,就是数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上,那么就是要读取的数据在内存里面的地址。如果是从硬盘读取数据到内存,那么就是硬盘的IO接口的地址
- IO的地址可以是一个内存地址,也可以是一个端口地址。而地址的增减就是说,数据时从大的地址向小的地址传输,还是从小的地址向大的地址传输
- 其次就是目标地址(数据传输的目的地)初始值和传输时候的地址增减方式。
- 第三个自然是要传输的数据长度,也就是我们一共要传输多少数据
- 首先是源地址的初始值和传输时候的地址增减方式
- 设置完这些信息之后,DMAC就会变成一个空闲的状态(Idle)
- 如果我们要从硬盘上往内存里面加载数据,这个时候,硬盘就会向 DMAC 发起一个数据传输请求。这个请求并不是通过总线,而是通过一个额外的连线
- 然后,我们的 DMAC 需要再通过一个额外的连线响应这个申请。
- 于是,DMAC 这个芯片,就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面,读到了 DMAC 的控制器里面。
- 然后,DMAC 再向我们的内存发起总线写的数据传输请求,把数据写入到内存里面。
- DMAC 会反复进行上面第 6、7 步的操作,直到 DMAC 的寄存器里面设置的数据长度传输完成。
- 数据传输完成之后,DMAC 重新回到第 3 步的空闲状态。
总结
以前的计算机里是没有 DMAC 的,所有数据都是由 CPU 来搬运的。随着对于数据传输的需求越来越多,先是出现了主板上独立的 DMAC 控制器。到了今天,各种 I/O 设备