undefined

一、CPU上下文

CPU执行任务需要知道从哪里开始,也就是说,需要系统帮它设置好CPU寄存器和程序计数器。
CPU寄存器是CPU内置的容量小、速度极快的内存。程序计数器用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。他们都是CPU在运行任务前,必须的依赖环境,也叫做CPU上下文。
那么,CPU上下文切换就是先把一个任务的CPU上下文(CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就保证任务原来的状态不受影响。

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

1. 进程上下文切换

如果让应用程序随便访问内存太危险了,因此按照CPU指令的重要程度对指令进行了分级,指令分为四个级别:Ring0-Ring3。
Linux只使用了 Ring0 和 Ring3 这两个运行级别。
进程运行在 Ring3 级别时被称为用户态,指令只能访问用户空间,被执行的代码要受到CPU很多检查;运行在 Ring0 级别时被称为内核态,可以执行任何指令,访问任何内存空间。
从用户态到内核态的转变,需要通过系统调用来完成。系统调用的过程会发生CPU上下文切换。
CPU寄存器中原来用户态的指令位置,需要先保存起来,然后更新为内核态指令的新位置,执行内核态代码;相反,从内核态切换到用户态也需要进行CPU上下文切换。因此,一次系统调用过程,发生了两次CPU上下文切换。

进程的上下文包括虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
而保存上下文和恢复上下文的过程是需要内核在CPU上运行才能完成的。而根据 Tsuna (https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html) 的测试报告,每次上下文切换都需要几十纳秒到数微妙的CPU时间。因此,如果进程上下文切换次数较多的情况下,很容易导致CPU将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。

Linux 下每个CPU都有一个就绪队列,将活跃进程(即正在运行和正在等待CPU的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待CPU时间最长的进程来运行。

进程调度的场景:

  1. CPU时间片,当某个进程的时间片耗尽了,就会被系统挂起,切换到其他正在等待CPU的进程运行
  2. 进程需要等待资源时候,也会被挂起,并由系统调度其他进程运行
  3. 当进程通过类似睡眠函数(sleep)这样的方法将自己主动挂起,也会重新调度
  4. 当有高优先级进程需要运行时,当前进程会被挂起,由高优先级进程来运行
  5. 发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中的中断服务服务

2. 线程上下文切换

线程是调度的基本单位,进程则是资源拥有的基本单位。因此

  1. 如果切换的线程属于两个不同的进程,则此切换过程和进程上下文切换是一样的
  2. 如果切换的线程属于同一个进程,则切换时,只需要切换线程的私有数据、寄存器等不共享的数据即可

3. 中断上下文切换

中断上下文只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。