上一节,我讲了要怎么理解平均负载,并用三个案例展示了不同场景下平均负载升高的分析方法。这其中,多个进程竞争CPU就是一个经常被我们忽视的问题。

我想你一定很好奇,进程在竞争CPU的时候并没有真正运行,为什么还会导致系统的负载升高呢?你应该已经猜到了,CPU上下文切换就是罪魁祸首。

我们都知道,linux是一个多任务操作系统,它支持远大于CPU数量的任务同时运行。当然,这些任务实际上并不时真的在同时运行,而是因为系统在很短的时间内,将CPU轮流分配给他们,造成多任务同时运行的错觉。

而在每个任务运行前,CPU都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统事先帮他设置好CPU寄存器和程序计数器(Program Counter,PC)。

CPU寄存器,是CPU内置的容量小、但速度极快的内存。而程序计数器,则是用来存储CPU正在执行的指令位置、或者即将执行的下一条指令的位置。他们都是CPU在运行任何任务前,必须的依赖环境,因此也被叫做CPU上下文。

CPU上下文切换,就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。 而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

一、关于上下文切换的几个为什么?

1、 上下文切换是什么?

上下文切换是对任务当前运行状态的暂存和恢复

2、 CPU为什么要进行上下文切换?

当多个进程竞争CPU的时候,CPU为了保证每个进程能公平被调度运行,采取了处理任务时间分片的机制,轮流处理多个进程,由于CPU处理速度非常快,在人类的感官上认为是并行处理,实际是"伪"并行,同一时间只有一个任务在运行处理。

3、 上下文切换主要消耗什么资源,为什么说上下文切换次数过多不可取?

根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到到微秒的CPU时间,这些时间对CPU来说,就好比人类对1分钟或10分钟的感觉概念。在分秒必争的计算机处理环境下,浪费太多时间在切换上,只能会降低真正处理任务的时间,表象上导致延时、排队、卡顿现象发生。

4、 上下文切换分几种?

进程上下文切换、线程上下文切换、中断上下文切换

5、 什么情况下会触发上下文切换?

系统调用、进程状态转换(运行、就绪、阻塞)、时间片耗尽、系统资源不足、sleep、优先级调度、硬件中断等

6、 线程上下文切换和进程上下文切换的最大区别?

线程是调度的基本单位,进程是资源拥有的基本单位,同属一个进程的线程,发生上下文切换,只切换线程的私有数据,共享数据不变,因此速度非常快。

7、 有哪些减少上下文切换的技术用例?

数据库连接池(复用连接)、合理设置应用的最大进程,线程数、直接内存访问DMA、零拷贝技术

二、上下文切换过程详解

1、进程上下文切换过程

(1)Linux按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中,CPU特权等级的Ring 0和Ring 3。

内核空间(Ring 0)具有最高权限,可以直接访问所有资源;内核空间态资源包括内核的堆栈、寄存器等

用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。用户空间态资源包括虚拟内存、栈、变量、正文、数据等

Linux性能优化实战学习笔记二_上下文切换

(2)系统调用(软中断)在内核态完成的,需要进行2次CPU上下文切换(用户空间-->内核空间-->用户空间),不涉及用户态资源,也不会切换进程。

系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU的上下文切换还是无法避免的。

Linux性能优化实战学习笔记二_上下文切换_02

(3)进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了用户空间的资源,也包括内核空间资源。

(4)进程的上下文切换过程:

(a)接收到切换信号,挂起进程,记录当前进程的虚拟内存、栈等资源存储;
(b)将这个进程在 CPU 中的上下文状态存储于起来;
(c)然后在内存中检索下一个进程的上下文;
(d)并将其加载到 CPU的寄存器中恢复;

(e)还需要刷新进程的虚拟内存和用户栈;

(f)最后跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

(5)、进程在什么时候才会被调度到CPU上运行呢?

(a)、为了保证所有进程可以得到公平调度,CPU时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其他正在等待CPU的进程运行。

(b)、进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他程序运行。

(c)、当进程通过睡眠函数sleep这样的方法将自己主动挂起时,自然也会重新调度。

(d)、当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。

(e)、发生硬件中断时,CPU上的进程会被中断挂起,转而执行内核中的中断服务程序。


2、线程上下文切换

线程与进程最大的区别在于,线程时调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于线程和进程,我们可以这样理解:

当进程只有一个线程时,可以任务进程等于线程。

当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。

另外,线程也有自己的私有 数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

线程的上下文切换可以分为两种情况:

第一种,前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换时一样。

第二种,前后两个线程属于同一个进程。此时,因为虚拟内存时共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

到这里你就应该发现了,虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这也正是多线程替代多进程的一个优势。

3、 中断上下文切换,如何理解?

为了快速响应硬件的事件(如USB接入),中断处理会打断进程的正常调度和执行,转而调用中断处理程序,

响应设备事件。而打断其它进程执行时,需要进行上下文切换。中断事件过多,会无谓的消耗CPU资源,导致进程处理时间延长。

对于一个CPU来说,中断处理比进程拥有更高的优先级