上下文切换

443 阅读6分钟

上下文是指CPU寄存器和程序计数器。它们是CPU在运行前,必须依赖的环境。

  1. CPU寄存器是CPU内置的容量小但是速度极快的的内存。
  2. 程序计数器是用来存储CPU正在执行的指令位置和即将执行的下一条指令的位置。 上下文切换是在CPU上对进程或者线程进行切换,将前一个任务的CPU上下文保存起来,然后加载新任务的上下文到CPU寄存器和程序计数器,最后跳转到程序计数器指的位置,运行新任务。

上下文切换的种类

根据任务的不同,上下文切换可以分为进程上下文切换、线程上下文切换、中断上下文切换。

进程上下文切换

进程上下文切换步骤

上下文切换的信息保存在进程控制块(PCB Process-Control Block),PCB又被称为切换帧,上下文切换的信息会一直保存在内存中,直至再次被使用。

  1. 保存进程A的状态(CPU寄存器和程序计数器)。
  2. 更新PCB中的信息,对进程A的运行态作出相应改变。
  3. 将进程A的PCB放入相关状态的队列。
  4. 将进程B的PCB信息改为运行态,并执行进程B。
  5. B执行完以后,从队列中取出进程A的PCB,恢复进程A被切换时的上下文,继续执行A。

进程上下文切换损耗

1. 用户态和内核态切换

操作系统将进程的运行空间分为用户空间和内核空间,用户空间访问内存资源是受限的,内核空间可以访问所有资源,如果用户想读写硬件资源,只能通过系统调用的方式进行状态切换。 整个系统调用的流程如下:

  1. 保存CPU寄存器里原来用户态的指令位。
  2. 为了执行内核态代码,CPU寄存器需要更新为内核态指令的新位置,跳转内核态运行内核任务。
  3. 任务执行结束,CPU寄存器需要恢复原来保存的用户态,然后在切换到用户空间,继续运行进程 一次系统调用,发生了两次状态切换(用户态-内核态-用户态)

系统调用并不会涉及虚拟内存等进程用户态的资源,也不会切换进程 进程上下文切换和用户态与内核态的CPU上下文切换有什么区别?

  1. 进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以进程的上下文不仅包括虚拟内存、栈、全局变量等用户空间的资源,还包括内存堆栈,寄存器等内核空间的状态。
  2. 进程的上下文切换比用户态和内核态的CPU切换多了一步,在保存内核态资源(当前进程的内核状态和CPU寄存器)之前,需要把该进程的用户态资源(虚拟内存和栈)保存下来;在加载了下一进程的内核态资源后,还需要刷新进程的虚拟内存和用户栈。

2. 虚拟内存

虚拟内存和物理内存的映射通过页表机制来实现,为了更快的操作虚拟内存,操作系统设计了TLB来管理虚拟内存和物理内存的映射,可以理解为一个高速缓存器,特点是速度快,存储的数据少,缓存有可能不命中,系统发生进程切换,从进程A切换到进程B,TLB也必须刷新,在刷新后,进程B会出现TLB不命中的情况,导致虚拟内存访问变慢。

3. 进程本身数据结构庞大

Linux使用数据结构task_struct来描述进程所有的资源,主要成员有进程状态、内核栈信息、进程使用状态、PID、锁、优先级、时间片、队列、信号量、内存管理信息、文件列表等与进程管理、调度密切相关的信息,当切换进程时,这些数据结构也需要保存和恢复

线程上下文切换

进程和线程最大的区别是:线程是调度的基本单位,进程是资源拥有的基本单位。其实内核中的任务调度,实际上的调度对象是线程,进程只是给线程提供了虚拟内存、全局变量等资源。

  • 当发生切换的两个线程不属于同一个进程,和两个进程切换的步骤一样。
  • 当两个线程属于同一个进程,因为虚拟内存是共享的,刷新TLB没有必要,切换成本就小。只需要切换线程的私有数据、寄存器等不共享的数据。

中断上下文切换

  • 为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。在打断进程时,需要将进程当前的状态保存下来,在中断结束后,进程仍然可以在原来的状态恢复运行。
  • 跟进程上下文切换不同的是,中断上下文切换并不涉及到用户态。所以即使打断处于用户态的进程,也不需要保存和恢复这个进程的虚拟内存,全局变量等用户资源。中断上下文其实只包括CPU寄存器、内核堆栈、硬件中断等参数。
  • 对于同一个CPU来说,中断处理比进程拥有更高的优先级,所以不会出现中断上下文切换与进程上下文切换同时发生。中断上下文也需要切换也需要消耗CPU,切换次数过多也会耗费大量CPU,甚至严重降低系统的总体性能。

上下文切换场景

切换分为两种,自愿和非自愿

自愿上下文切换

  • 当前进程所需要的资源未满足时,会挂起等待。
  • 调用睡眠函数sleep这样的方法将自己主动挂起。

非自愿上下文切换

  • 为了保证所有的进程可以得到公平调度,会将CPU时间分片,轮流分给每个进程,若进程的时间片耗尽了,会被系统挂起,强制切换到其他正在等待CPU的进程;
  • 当有更高优先级的进程运行时,为了保证高优先级进程的运行,会把当前进程挂起,由高优先级进程运行;
  • 当发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中的中断服务程序。

如何减少上下文切换

  • 无锁并发编程。 多线程竞争锁时会引起上下文切换,所以多线程处理数据时,可以用一些方法避免使用锁,比如将数据的ID按照hash算法,取模分段,不同的线程处理不同段的数据。
  • CAS算法。 使用Java中Atomic包中的CAS算法更新数据,不需要使用锁。
  • 使用最少线程。 避免创建不需要的线程。
  • 使用协程。 在单线程里实现多任务调度,并在单线程里维持多个任务的切换。

参考:上下文切换总结 - 掘金 (juejin.cn)