我们都知道Linux是一个多任务操作系统,它支持同时运行的任务数远远大于CPU的数量。当然,这些任务实际上并不是同时运行(SingleCPU),而是因为系统在短时间内轮流分配CPU给任务,造成了多个任务同时运行的错觉。CPU上下文(CPUContext)在每个任务运行之前,CPU需要知道从哪里加载和启动任务。这意味着系统需要帮助提前设置CPU寄存器和程序计数器。CPU寄存器是内置于CPU中的很小但速度极快的内存块。程序计数器用于存储CPU正在执行的指令或下一条要执行的指令的位置。它们都是CPU在运行任何任务之前必须依赖的依赖环境,因此也被称为“CPU上下文”。如下图所示:知道了CPU上下文是什么,我想大家理解CPU上下文切换就容易多了。“CPU上下文切换”是指先保存上一个任务的CPU上下文(CPU寄存器和程序计数器),然后将新任务的上下文加载到这些寄存器和程序计数器中,最后跳转到程序计数器。这些保存的上下文存储在系统内核中,并在重新安排任务执行时再次加载。这确保了任务的原始状态不受影响,并且任务看起来一直在运行。CPU上下文切换的类型你可能会说CPU上下文切换无非是更新CPU寄存器和程序计数器的值,而这些寄存器是为了快速运行任务而设计的,那么为什么会影响CPU性能呢?在回答这个问题之前,请问大家有没有想过这些“任务”是什么?您可能会说任务是进程或线程。是的,进程和线程是最常见的任务,但还有其他类型的任务。不要忘记硬件中断也是一个常见的任务,硬件触发信号导致中断处理程序被调用。所以至少有三种不同类型的CPU上下文切换:进程上下文切换线程上下文切换中断上下文切换让我们一一来看。进程上下文切换Linux将进程的运行空间按照权限级别分为内核空间和用户空间,对应下图中Ring0和Ring3的CPU权限级别。内核空间(Ring0)拥有最高权限,可以直接访问所有资源。用户空间(Ring3)只能访问受限资源,不能直接访问内存等硬件设备。它必须通过系统调用被困(trapped)在内核中才能访问这些特权资源。从另一个角度来看,一个进程既可以运行在用户空间,也可以运行在内核空间。进程运行在用户空间时,称为进程的用户态,落入内核空间时,称为进程的内核态。从用户态到内核态的转换需要通过系统调用来完成。例如,当我们查看一个文件的内容时,我们需要以下系统调用:open():打开文件read():读取文件内容write():将文件内容写入输出文件(包括标准输出)close():关闭文件那么在上面的系统调用过程中会不会发生CPU上下文切换呢?当然。这需要首先保存原始用户模式指令在CPU寄存器中的位置。接下来,为了执行内核模式代码,CPU寄存器需要更新到内核模式指令的新位置。最后是跳转到内核态运行内核任务。那么系统调用结束后,CPU寄存器需要恢复原来保存的用户态,然后切换到用户空间继续运行进程。因此,在一次系统调用过程中,实际上有两次CPU上下文切换。但需要指出的是,系统调用过程不涉及进程切换,也不涉及虚拟内存等系统资源的切换。这与我们通常所说的“进程上下文切换”不同。进程上下文切换是指从一个进程切换到另一个进程,而同一个进程在系统调用期间一直在运行。系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,在系统调用的过程中,CPU上下文切换也是不可避免的。进程上下文切换vs系统调用那么进程上下文切换和系统调用有什么区别呢?首先,进程是由内核管理的,进程切换只能发生在内核态。因此,进程上下文不仅包括虚拟内存、栈、全局变量等用户空间资源,还包括内核栈、寄存器等内核空间的状态。所以进程上下文切换比系统调用多了一步:在保存当前进程的内核态和CPU寄存器之前,需要保存进程的虚拟内存、堆栈等;并且需要加载下一个进程的内核态。根据Tsuna的测试报告,每次上下文切换需要数十纳秒到微秒的CPU时间。这个时间是相当可观的,尤其是在大量进程上下文切换的情况下,很容易导致CPU花费大量时间保存和恢复寄存器、内核栈、虚拟内存等资源。这也正是我们在上一篇文章中谈到的,导致loadaverage上升的一个重要因素。那么,进程何时会被调度/切换到CPU上运行呢?其实场景有很多种,下面我给大家总结一下:当一个进程的CPU时间片用完后,会被系统挂起,切换到其他进程等待CPU运行。当系统资源不足时(如内存不足),只有资源充足时,进程才能运行。这时进程也会被挂起,系统会调度其他进程运行。当一个进程通过sleep函数自动挂起自己时,自然会被重新调度。当有更高优先级的进程运行时,为了保证高优先级进程的运行,当前进程会被高优先级进程挂起。当硬件中断发生时,CPU上的进程会被中断挂起,然后执行内核中的中断服务程序。了解这些场景是非常有必要的,因为一旦上下文切换出现性能问题,他们就是幕后杀手。线程上下文切换线程和进程最大的区别在于,线程是任务调度的基本单位,而进程是资源获取的基本单位。说白了,所谓内核中的任务调度,实际的调度对象是线程;而进程只是为线程提供虚拟内存和全局变量等资源。那么,对于线程和进程,我们可以这样理解:当一个进程只有一个线程时,可以认为一个进程等于一个线程。当一个进程有多个线程时,这些线程共享相同的资源,例如虚拟内存和全局变量。另外,线程也有自己的私有数据,比如栈、寄存器等,在上下文切换时也需要保存。这样,线程的上下文切换其实可以分为两种情况:第一,前后两个线程属于不同的进程。此时,由于没有共享资源,所以切换过程与进程上下文切换是一样的。第二,前后两个线程属于同一个进程。此时,由于虚拟内存是共享的,切换时虚拟内存的资源保持不变,只需要切换线程的私有数据、寄存器等非共享数据即可。显然,同一个进程内的线程切换比切换多个进程消耗的资源更少。这也是多线程代替多处理的优势。中断上下文切换除了前面两种上下文切换,还有一种场景也会输出CPU上下文切换,那就是中断。为了快速响应事件,硬件中断中断正常的调度和执行过程,然后调用中断处理程序。当中断其他进程时,需要保存进程的当前状态,以便进程在中断后仍能从原来的状态恢复。与进程上下文不同,中断上下文切换不涉及进程的用户空间。因此,即使中断进程打断了用户态的进程,也不需要保存和恢复进程的虚拟内存、全局变量等用户态资源。另外,和进程上下文切换一样,中断上下文切换也会消耗CPU。过多的切换次数会消耗大量的CPU资源,甚至会严重降低系统的整体性能。因此,当你发现中断过多时,需要注意是否会对你的系统造成严重的性能问题。结语综上所述,不管是哪种场景引起上下文切换,大家应该知道,CPU上下文切换是保证Linux系统正常运行的核心功能之一,一般不需要我们特别关注。但是过多的上下文切换会消耗CPU时间来保存和恢复寄存器、内核栈、虚拟内存等数据,从而缩短进程的实际运行时间,导致系统整体性能大幅下降。
