# 【NO.558】通过实战理解CPU上下文切换

Linux是一个多任务的操作系统，可以支持远大于CPU数量的任务同时运行，但是我们都知道这其实是一个错觉，真正是系统在很短的时间内将CPU轮流分配给各个进程，给用户造成多任务同时运行的错觉。所以这就是有一个问题，在每次运行进程之前CPU都需要知道进程从哪里加载、从哪里运行，也就是说需要系统提前帮它设置好CPU寄存器和程序计数器

## 1.CPU上下文

CPU上下文其实是一些环境正是有这些环境的支撑，任务得以运行，而这些环境的硬件条件便是CPU寄存器和程序计数器。CPU寄存器是CPU内置的容量非常小但是速度极快的存储设备，程序计数器则是CPU在运行任何任务时必要的，里面记录了当前运行任务的行数等信息，这就是CPU上下文

![img](https://pic1.zhimg.com/80/v2-be79c25ed9aae9f8981179cda6836870_720w.webp)

## 2.CPU上下文切换

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

### 2.1 进程上下文切换

在Linux中，Linux按照特权等级，将进程的运行空间分为内核空间和用户空间：

- 内核空间具有最高权限，可以直接访问所有资源
- 用户空间只能访问受限资源，不能直接访问内存等硬件设备，要想访问这些特权资源，必须通过系统调用

对于一个进程来说，一般是运行在用户态的，但是当需要访问内存、磁盘等硬件设备的时候需要陷入到内核态中，也就是要从用户态到内核态的转变，而这种转变需要通过系统调用来实现，例如一个打开文件的操作，需要调用open()打开文件，read()读取文件内容，write()将文件内容输出到控制台，最后close()关闭文件，这就是系统调用

在系统调用的过程中同样发发生了CPU上下文切换：

- CPU寄存器里面原来用户态的指令位置，需要先保存起来，接着运行内核态代码
- CPU寄存器需要更新为内核态指令的位置，执行内核态代码

系统调用结束后，CPU寄存器需要恢复原来保存的用户态，然后切换为用户空间，所以一次系统调用的过程，会发生两次的CPU上下文切换

但是我们一般说系统调用是特权模式切换而不是上下文切换，因为这里没有涉及到虚拟内存等这些进程用户态的资源，也不会切换进程是属于进程之内的上下文切换

进程是由内核来管理和调度的，进程的切换只能发生在内核态，所以进程的上下文包含了虚拟内存、栈、全局变量等用户空间的资源，还包含了内核堆栈、寄存器等内核空间的状态，所以进程的上下文切换要比系统调用更多一步，保存该进程的虚拟内存、栈等用户空间的资源

进程上下文切换一般需要几十纳秒到数微秒的CPU时间，当进程上下文切换次数比较多的情况下爱，将导致CPU将大量的时间耗费在寄存器、内核栈即虚拟内存等资源的保存和恢复上，另外，Linux通过TLB快表来管理虚拟内存到物理内存的映射关系，当虚拟内存更新之后，需要刷新缓存，在这多处理系统上是很复杂的，因为多个处理器共享一个缓存

下面再来说说什么时候会进行进程的上下文切换，其实就是进程在被调度的时候需要切换上下文，可能是主动地，也有可能是被动的

- 系统进程正常调度算法导致进程上下文切换，例如目前使用的时间片轮转算法，当一个进程的时间片耗尽之后，CPU会进项进程的调度切换到其他进程
- 进程在资源不足的时候，会被挂起例如在等待IO或者内存不足的时候，会主动挂起，并且等待系统调度其他进程
- 当进程通过一些睡眠函数sleep()主动挂起的时候，也会重新调度
- 当有高优先级的进程运行时，当前进程也会被挂起
- 当发生硬件中断时，CPU上的进程会被中断挂起

### 2.2 线程上下文切换

线程是调度的基本单位，而进程则是资源拥有的基本单位，也就是说对于内核中的任务调度是以线程为单位，但是进程只是给线程提供了虚拟内存、全局变量等资源，进程与线程之间的区别这里不再介绍

那么线程上下文的切换，其实分为两种情况：

- 前后两个线程属于不同进程，因为资源不共享，所以这时候的线程上下文切换和进程上下文切换是一致的
- 前后两个线程属于同一个进程，因为虚拟内存是共享的，所以在切换的时候，虚拟内存这些资源保持不动，只有切换线程的私有数据、寄存器等不共享的资源

所以同进程内的线程切换要比多进程内的线程切换消耗更少的资源

### 2.3 中断上下文切换

中断是为了快速响应硬件的事件，简单来shu就是计算机停下当前的事情，去处理其他的事情，然后在回来继续执行之前的任务，例如我们在调用print函数的时候，其实汇编的底层会帮我们调用一条 int 0x80的指令，便是调用0x80号中断

当然，中断要先将当前进程的状态保存下来，这样中断结束后进程仍然可以从原来的状态恢复运行，中断上下文的切换并不涉及进程的用户态，所以当中断程序打断了正在处于用户态的进程，不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源，只需要保存和恢复这个进程的内核态中的资源包括CPU寄存器、内核堆栈等

对于同一个CPU来说，中断处理比进程拥有更高的优先级，所以中断上下文切换并不会与进程上下文切换同时发生，一般来说中断程序都执行比较快短小精悍，以便快速结束执行之前的任务。当中断上下文切换次数比较多的时候，会耗费大量的CPU

### 2.4 怎么查看系统上下文

上面已经介绍到CPU上下文切换分为进程上下文切换、线程上下文切换、中断上下文切换，那么过多的上下文切换会把CPU的时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上，缩短进程真正运行的时间，成为系统性能大幅下降的一个因素

所以我们可以使用vmstat这个工具来查询系统的上下文切换情况，vmstat是一个常用的系统性能分析工具，可以用来分析CPU上下文切换和中断的次数

![img](https://pic4.zhimg.com/80/v2-97b24494c5c70f691fef0d4e6de62d3f_720w.webp)

需要特别关注的是：

- cs(context switch)：每秒上下文切换的次数
- in(interrupt)：每秒中断的次数
- r(Running or Runnable)：就绪队列的长度，也就是正在运行和等待CPU的进程
- b(Blocked)：处于不可中断睡眠状态的进程数

vmstat是给出整个系统总体的上下文切换情况，要想查看每个进程的详细情况就需要使用pidstat，加上-w选项就可以查看进程上下文切换的情况

![img](https://pic1.zhimg.com/80/v2-33ff027184b6b9e1cf4fffeaaccf1e14_720w.webp)

需要特别关注的是：

- cswch(voluntary context switches)：表示每秒自愿上下文切换的次数
- nvcswch(non voluntary context switches)：表示每秒非自愿上下文切换的次数

这两个概念的分别含义：

- 自愿上下文切换：进程无法获取所需的资源，导致的上下文切换，例如IO、内存等资源不足时，就会发生自愿上下文切换
- 非自愿上下文切换：进程由于时间片已到等时间，被系统强制调度，进而发生的上下文切换，例如大量的进程都在争抢CPU时，就容易发生非自愿上下文切换

## 3.实战分析

通过上面的工具已经可以初步查看到系统上下文切换的次数，但是当系统上下文切换的次数为多少时是不正常的呢？

案例使用sysbench工具来模拟多线程调度切换的情况，sysbench是一个多线程的基准测试工具，可以模拟上下文切换过多的问题

首先在第一个终端运行stsbench，模拟多线程切换问题

```text
# 以 10 个线程运行 5 分钟的基准测试，模拟多线程切换的问题
sysbench --threads=10 --max-time=300 threads run
```

然后在第二个终端运行vmstat，每1秒查看上下文切换的情况

![img](https://pic1.zhimg.com/80/v2-cea4f145d8738f60ce2aabe41fefa574_720w.webp)

可以观察到如下指标：

- r列：就绪队列的长度已经到了8左右，已经超过了2个cpu，所以会有大量的CPU竞争
- us(user)列和sy(system)列，这两列的CPU使用率已经到达100%，并且大量是由sy造成的，说明CPU主要是被内核占用了
- in(interrupt)：in列的数值也到了解决1万，所以中断处理也是一个问题

那我们接着使用pidstat来查看是那一个进程出现了问题，由于pidstat默认是显示进程的指标数据，但是我们使用sysbench模拟的线程的数据，所以需要加上-t选项

```text
gpw@gopuwe:~$ pidstat -wt
```

![img](https://pic4.zhimg.com/80/v2-5597fd751b16a8bdafe379615547e403_720w.webp)

所以到这里可以分析出是sysbench的子线程的上下文切换次数有很多

还有一个问题，在使用vmstat的时候，发现in指标的数据也比较多，那么我们需要找出是什么类型的中断导致了中断上升，中断肯定是发生在内核态，但是pidstat只是一个进程的性能分析工具，并不提供任何关于中断的详细信息

我们可以从/proc/interrupts这个只读文件中读取，/proc是一个虚拟文件系统，用于内核空间和用户空间之间的通信，/proc/interrupts则提供了一个只读的中断使用情况，可以使用cat命令查看/proc/interrupts可以发现变化速度最快的是重调度中断RES，这个中断类型表示唤醒空闲状态的CPU来调度新的任务运行，也被成为处理器中断

那么到底上下文切换的次数为多少合适呢？

这个数值其实取决于系统本身的 CPU 性能，在我看来，如果系统的上下文切换次数比较稳

定，那么从数百到一万以内，都应该算是正常的。但当上下文切换次数超过一万次，或者切

换次数出现数量级的增长时，就很可能已经出现了性能问题，这个时候还要根据上下文切换的类型，做具体的分析，例如：

- 自愿上下文切换变多了，说明进程都在等待资源，有可能发生了 I/O 等其他问题；
- 非自愿上下文切换变多了，说明进程都在被强制调度，也就是都在争抢 CPU，说明 CPU的确成了瓶颈；
- 中断次数变多了，说明 CPU 被中断处理程序占用，还需要通过查看 /proc/interrupts 文件

原文地址：https://zhuanlan.zhihu.com/p/444801883

作者：linux