02什么是CPU上下文切换
上⼀节, 讲了要怎么理解平均负载( Load Average) , 并⽤三个案例展示了不同场景下平均负载升⾼的分析⽅法。 这其中, 多个进程竞争 CPU 就是⼀个经常被我们忽视的问题。
1、CPU上下文切换的概念
我想你⼀定很好奇, 进程在竞争 CPU 的时候并没有真正运⾏, 为什么还会导致系统的负载升⾼呢? 看到今天的主题, 你应该已经猜到了, CPU 上下⽂切换就是罪魁祸⾸。
我们都知道, Linux 是⼀个多任务操作系统, 它⽀持远⼤于 CPU 数量的任务同时运⾏。 当然, 这些任务实际上并不是真的在同时运⾏, ⽽是因为系统在很短的时间内, 将 CPU 轮流分配给它们, 造成多任务同时运⾏的错觉。
⽽在每个任务运⾏前, CPU 都需要知道任务从哪⾥加载、 ⼜从哪⾥开始运⾏, 也就是说, 需要系统事先帮它设置好 CPU 寄存器和程序计数器(Program Counter, PC) 。
CPU 寄存器, 是 CPU 内置的容量⼩、 但速度极快的内存。 ⽽程序计数器, 则是⽤来存储 CPU 正在执⾏的指令位置、 或者即将执⾏的下⼀条指令位置。 它们都是 CPU 在运⾏任何任务前, 必须的依赖环境, 因此也被叫做 CPU 上下⽂。
知道了什么是 CPU 上下⽂, 我想你也很容易理解 CPU 上下⽂切换。 CPU 上下⽂切换, 就是先把前⼀个任务的 CPU 上下⽂(也就是 CPU 寄存器和程序计数器) 保存起来, 然后加载新任务的上下⽂到这些寄存器和程序计数器, 最后再跳转到程序计数器所指的新位置, 运⾏新任务。
⽽这些保存下来的上下⽂, 会存储在系统内核中, 并在任务重新调度执⾏时再次加载进来。 这样就能保证任务原来的状态不受影响, 让任务看起来还是连续运⾏。
我猜肯定会有⼈说, CPU 上下⽂切换⽆⾮就是更新了 CPU 寄存器的值嘛, 但这些寄存器, 本身就是为了快速运⾏任务⽽设计的, 为什么会影响系统的 CPU 性能呢?
在回答这个问题前, 不知道你有没有想过, 操作系统管理的这些“任务”到底是什么呢?
也许你会说, 任务就是进程, 或者说任务就是线程。 是的, 进程和线程正是最常⻅的任务。 但是除此之外, 还有没有其他的任务呢?
不要忘了, 硬件通过触发信号, 会导致中断处理程序的调⽤, 也是⼀种常⻅的任务。
2、CPU上下文切换的分类
所以, 根据任务的不同, CPU 的上下⽂切换就可以分为⼏个不同的场景, 也就是进程上下⽂切换、 线程上下⽂切换以及中断上下⽂切换。
这节课我就带你来看看, 怎么理解这⼏个不同的上下⽂切换, 以及它们为什么会引发 CPU 性能相关问题。
2.1 进程上下⽂切换
Linux 按照特权等级, 把进程的运⾏空间分为内核空间和⽤户空间, 分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring3。
-
内核空间(Ring 0) 具有最⾼权限, 可以直接访问所有资源;
-
⽤户空间(Ring 3) 只能访问受限资源, 不能直接访问内存等硬件设备, 必须通过系统调⽤陷⼊到内核中, 才能访问这些特权资源。
换个⻆度看, 也就是说, 进程既可以在⽤户空间运⾏, ⼜可以在内核空间中运⾏。 进程在⽤户空间运⾏时, 被称为进程的⽤户态, ⽽陷⼊内核空间的时候, 被称为进程的内核态。
从⽤户态到内核态的转变, 需要通过系统调⽤来完成。 ⽐如, 当我们查看⽂件内容时, 就需要多次系统调⽤来完成: ⾸先调⽤open() 打开⽂件, 然后调⽤ read() 读取⽂件内容, 并调⽤ write() 将内容写到标准输出, 最后再调⽤ close() 关闭⽂件。
那么, 系统调⽤的过程有没有发⽣ CPU 上下⽂的切换呢? 答案⾃ 然是肯定的。
CPU 寄存器⾥原来⽤户态的指令位置, 需要先保存起来。 接着, 为了执⾏内核态代码, CPU 寄存器需要更新为内核态指令的新位置。 最后才是跳转到内核态运⾏内核任务。
⽽系统调⽤结束后, CPU寄存器需要恢复原来保存的⽤户态, 然后再切换到⽤户空间, 继续运⾏进程。 所以, ⼀次系统调⽤的过程, 其实是发⽣了两次 CPU 上下⽂切换。
不过, 需要注意的是, 系统调⽤过程中, 并不会涉及到虚拟内存等进程⽤户态的资源, 也不会切换进程。 这跟我们通常所说的进程上下⽂切换是不⼀样的:
进程上下⽂切换, 是指从⼀个进程切换到另⼀个进程运⾏。
⽽系统调⽤过程中⼀直是同⼀个进程在运⾏。
所以, 系统调⽤过程通常称为特权模式切换, ⽽不是上下⽂切换。 但实际上, 系统调⽤过程中, CPU 的上下⽂切换还是⽆法避免的。
那么, 进程上下⽂切换跟系统调⽤⼜有什么区别呢?
⾸先, 你需要知道, 进程是由内核来管理和调度的, 进程的切换只能发⽣在内核态。 所以, 进程的上下⽂不仅包括了虚拟内存、 栈、 全局变量等⽤户空间的资源, 还包括了内核堆栈、 寄存器等内核空间的状态。
因此, 进程的上下⽂切换就⽐系统调⽤时多了⼀步: 在保存当前进程的内核状态和CPU寄存器之前, 需要先把该进程的虚拟内存、 栈等保存下来; ⽽加载了下⼀进程的内核态后, 还需要刷新进程的虚拟内存和⽤户栈。
如下图所示, 保存上下⽂和恢复上下⽂的过程并不是“免费”的, 需要内核在 CPU 上运⾏才能完成。
根据 Tsuna 的测试报告, 每次上下⽂切换都需要⼏⼗纳秒到数微秒的 CPU 时间。 这个时间还是相当可观的, 特别是在进程上下⽂切换次数较多的情况下, 很容易导致 CPU 将⼤量时间耗费在寄存器、 内核栈以及虚拟内存等资源的保存和恢复上, 进⽽⼤⼤缩短了真正运⾏进程的时间。 这也正是上⼀节中我们所讲的, 导致平均负载升⾼的⼀个重要因素。
另外, 我们知道, Linux 通过 TLB(Translation Lookaside Buffer) 来管理虚拟内存到物理内存的映射关系。 当虚拟内存更新后, TLB 也需要刷新, 内存的访问也会随之变慢。 特别是在多处理器系统上, 缓存是被多个处理器共享的, 刷新缓存不仅会影响当前处理器的进程, 还会影响共享缓存的其他处理器的进程。
知道了进程上下⽂切换潜在的性能问题后, 我们再来看, 究竟什么时候会切换进程上下⽂。
显然, 进程切换时才需要切换上下⽂, 换句话说, 只有在进程调度的时候, 才需要切换上下⽂。 Linux 为每个 CPU 都维护了⼀个就绪队列, 将活跃进程(即正在运⾏和正在等待CPU的进程) 按照优先级和等待 CPU 的时间排序, 然后选择最需要CPU 的进程, 也就是优先级最⾼和等待CPU时间最⻓的进程来运⾏。
那么, 进程在什么时候才会被调度到 CPU 上运⾏呢?
最容易想到的⼀个时机, 就是进程执⾏完终⽌了, 它之前使⽤的CPU会释放出来, 这个时候再从就绪队列⾥, 拿⼀个新的进程过来运⾏。 其实还有很多其他场景, 也会触发进程调度, 在这⾥我给你逐个梳理下。
- 其⼀, 为了保证所有进程可以得到公平调度, CPU时间被划分为⼀段段的时间⽚, 这些时间⽚再被轮流分配给各个进程。 这样, 当某个进程的时间⽚耗尽了, 就会被系统挂起, 切换到其它正在等待 CPU 的进程运⾏。
- 其⼆, 进程在系统资源不⾜(⽐如内存不⾜) 时, 要等到资源满⾜后才可以运⾏, 这个时候进程也会被挂起, 并由系统调度其他进程运⾏。
- 其三, 当进程通过睡眠函数 sleep 这样的⽅法将⾃ ⼰主动挂起时, ⾃然也会重新调度。
- 其四, 当有优先级更⾼的进程运⾏时, 为了保证⾼优先级进程的运⾏, 当前进程会被挂起, 由⾼优先级进程来运⾏。
- 最后⼀个, 发⽣硬件中断时, CPU上的进程会被中断挂起, 转⽽执⾏内核中的中断服务程序。
了解这⼏个场景是⾮常有必要的, 因为⼀旦出现上下⽂切换的性能问题, 它们就是幕后凶⼿。
2.2 线程上下⽂切换
说完了进程的上下⽂切换, 我们再来看看线程相关的问题。
线程与进程最⼤的区别在于, 线程是调度的基本单位, ⽽进程则是资源拥有的基本单位。 说⽩了, 所谓内核中的任务调度, 实际上的调度对象是线程; ⽽进程只是给线程提供了虚拟内存、 全局变量等资源。 所以, 对于线程和进程, 我们可以这么理解:
当进程只有⼀个线程时, 可以认为进程就等于线程。
当进程拥有多个线程时, 这些线程会共享相同的虚拟内存和全局变量等资源。 这些资源在上下⽂切换时是不需要修改的。
另外, 线程也有⾃⼰的私有数据, ⽐如栈和寄存器等, 这些在上下⽂切换时也是需要保存的。
这么⼀来, 线程的上下⽂切换其实就可以分为两种情况:
第⼀种, 前后两个线程属于不同进程。 此时, 因为资源不共享, 所以切换过程就跟进程上下⽂切换是⼀样。
第⼆种, 前后两个线程属于同⼀个进程。 此时, 因为虚拟内存是共享的, 所以在切换时, 虚拟内存这些资源就保持不动, 只需要切换线程的私有数据、 寄存器等不共享的数据。
到这⾥你应该也发现了, 虽然同为上下⽂切换, 但同进程内的线程切换, 要⽐多进程间的切换消耗更少的资源, ⽽这, 也正是多线程代替多进程的⼀个优势。
2.3 中断上下⽂切换
除了前⾯两种上下⽂切换, 还有⼀个场景也会切换 CPU 上下⽂, 那就是中断。
为了快速响应硬件的事件, 中断处理会打断进程的正常调度和执⾏, 转⽽调⽤中断处理程序, 响应设备事件。 ⽽在打断其他进程时, 就需要将进程当前的状态保存下来, 这样在中断结束后, 进程仍然可以从原来的状态恢复运⾏。
跟进程上下⽂不同, 中断上下⽂切换并不涉及到进程的⽤户态。 所以, 即便中断过程打断了⼀个正处在⽤户态的进程, 也不需要保存和恢复这个进程的虚拟内存、 全局变量等⽤户态资源。 中断上下⽂, 其实只包括内核态中断服务程序执⾏所必需的状态, 包括CPU 寄存器、 内核堆栈、 硬件中断参数等。
对同⼀个 CPU 来说, 中断处理⽐进程拥有更⾼的优先级, 所以中断上下⽂切换并不会与进程上下⽂切换同时发⽣。 同样道理, 由于中断会打断正常进程的调度和执⾏, 所以⼤部分中断处理程序都短⼩精悍, 以便尽可能快的执⾏结束。
另外, 跟进程上下⽂切换⼀样, 中断上下⽂切换也需要消耗CPU, 切换次数过多也会耗费⼤量的 CPU, 甚⾄严重降低系统的整体性能。 所以, 当你发现中断次数过多时, 就需要注意去排查它是否会给你的系统带来严重的性能问题。
3、怎么查看系统的上下⽂切换情况
通过前⾯学习我们知道, 过多的上下⽂切换, 会把CPU 时间消耗在寄存器、 内核栈以及虚拟内存等数据的保存和恢复上, 缩短进程真正运⾏的时间, 成了系统性能⼤幅下降的元凶。
既然上下⽂切换对系统性能影响那么⼤, 到底要怎么查看上下⽂切换呢? 在这⾥, 我们可以使⽤vmstat 这个⼯具, 来查询系统的上下⽂切换情况。
如何确认vmstat的安装包呢?
# sudo apt install -y apt-file
# sudo apt-file update
# sudo apt-file search vmstat
...
procps: /usr/share/man/man8/vmstat.8.gz
...
以上就可以确认vmstat命令是需要安装procps安装包
# 安装vmstat命令:
sudo apt install -y procps
sudo yum install -y proc-ng
vmstat 命令是 procp-ng 软件包的⼀部分,其中包含其他⼀些实⽤的性能分析命令,如 free 和 top。vmstat(VirtualMeomoryStatistics,虚拟内存统计)是Linux中监控内存的常用工具,可对操作系统的虚拟内存、进程、CPU等的整体情况进行监视。
如果不使⽤任何参数,vmstat 命令将显⽰⾃启动以来系统各个统计信息的平均值。vmstat 命令可接受似于 sysstat 软件包实⽤程序的两个参数。其中,第⼀个参数为 delay 选项,第⼆个参数为 count 选项。
默认情况下,内存统计信息将以 KiB 为单位来报告。通过 -S 选项,可分别使⽤ -S k、-S m 和 -S M 选项将报告单位变更为 KB、MB 或 MiB。
命令参数详解:
vmstat [options] [delay [count] ]
-a:显示活跃和非活跃内存
-f:显示从系统启动至今的fork数量 。
-m:显示slabinfo
-n:只在开始时显示一次各字段名称。
-s:显示内存相关统计信息及多种系统活动数量。
delay:刷新时间间隔。如果不指定,只显示一条结果。
count:刷新次数。如果不指定刷新次数,但指定了刷新时间间隔,这时刷新次数为无穷。
-d:显示磁盘相关统计信息。
-p:显示指定磁盘分区统计信息
-S:使用指定单位显示。默认单位为K(1024 bytes)。
vmstat 是⼀个常⽤的系统性能分析⼯具, 主要⽤来分析系统的内存使⽤情况, 也常⽤来分析 CPU 上下⽂切换和中断的次数。⽐如, 下⾯就是⼀个 vmstat 的使⽤示例:
# 每隔5秒输出1组数据
$ vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
我们⼀起来看这个结果, 你可以先试着⾃ ⼰解读每列的含义。 在这⾥, 我重点强调下, 需要特别关注的四列内容:
cs(context switch) 是每秒上下⽂切换的次数。
in(interrupt) 则是每秒中断的次数。
r(Running or Runnable) 是就绪队列的⻓度, 也就是正在运⾏和等待CPU的进程数。
b(Blocked) 则是处于不可中断睡眠状态的进程数。
可以看到, 这个例⼦中的上下⽂切换次数 cs 是33次, ⽽系统中断次数 in 则是25次, ⽽就绪队列⻓度r和不可中断状态进程数b都是0。
vmstat 只给出了系统总体的上下⽂切换情况, 要想查看每个进程的详细情况, 就需要使⽤我们前⾯提到过的 pidstat 了。 给它加上 -w 选项, 你就可以查看每个进程上下⽂切换的情况了。⽐如说:
# 每隔5秒输出1组数据
$ pidstat -w 5
Linux 4.15.0 (ubuntu) 09/23/18 _x86_64_ (2 CPU)
08:18:26 UID PID cswch/s nvcswch/s Command
08:18:31 0 1 0.20 0.00 systemd
08:18:31 0 8 5.40 0.00 rcu_sched
...
这个结果中有两列内容是我们的重点关注对象。 ⼀个是 cswch , 表示每秒⾃愿上下⽂切换(voluntary context switches) 的次数, 另⼀个则是 nvcswch , 表示每秒⾮⾃愿上下⽂切换(non voluntary context switches) 的次数。
这两个概念意味着不同的性能问题:
所谓⾃愿上下⽂切换, 是指进程⽆法获取所需资源, 导致的上下⽂切换。 ⽐如说, I/O、 内存等系统资源不⾜时, 就会发⽣⾃ 愿上下⽂切换。
⽽⾮⾃愿上下⽂切换, 则是指进程由于时间⽚已到等原因, 被系统强制调度, 进⽽发⽣的上下⽂切换。 ⽐如说, ⼤量进程都在争抢 CPU 时, 就容易发⽣⾮⾃愿上下⽂切换。
4、案例分析
知道了怎么查看这些指标, 另⼀个问题⼜来了, 上下⽂切换频率是多少次才算正常呢? 别急着要答案, 同样的, 我们先来看⼀个上下⽂切换的案例。 通过案例实战演练, 你⾃ ⼰就可以分析并找出这个标准了。
你的准备
今天的案例, 我们将使⽤ sysbench 来模拟系统多线程调度切换的情况。
sysbench 是⼀个多线程的基准测试⼯具, ⼀般⽤来评估不同系统参数下的数据库负载情况。 当然, 在这次案例中, 我们只把它当成⼀个异常进程来看, 作⽤是模拟上下⽂切换过多的问题。
下⾯的案例基于 Ubuntu 18.04, 当然, 其他的 Linux 系统同样适⽤。 我使⽤的案例环境如下所示:
- 机器配置: 2 CPU, 8GB 内存
- 预先安装 sysbench 和 sysstat 包, 如 apt install sysbench sysstat
正式操作开始前, 你需要打开三个终端, 登录到同⼀台 Linux 机器中, 并安装好上⾯提到的两个软件包。
另外注意, 下⾯所有命令, 都默认以 root ⽤户运⾏。 所以, 如果你是⽤普通⽤户登陆的系统, 记住先运⾏ sudo su root 命令切换到 root ⽤户。
安装完成后, 你可以先⽤ vmstat 看⼀下空闲系统的上下⽂切换次数:
# 间隔1秒后输出1组数据
$ vmstat 1 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 0 0 6984064 92668 830896 0 0 2 19 19 35 1 0 99 0 0
这⾥你可以看到, 现在的上下⽂切换次数 cs 是35, ⽽中断次数 in 是19, r和b都是0。 因为这会⼉我并没有运⾏其他任务, 所以它们就是空闲系统的上下⽂切换次数。
操作和分析
接下来, 我们正式进⼊实战操作。
⾸先, 在第⼀个终端⾥运⾏ sysbench , 模拟系统多线程调度的瓶颈:
# 以10个线程运行5分钟的基准测试,模拟多线程切换的问题
$ sysbench --threads=10 --max-time=300 threads run
接着, 在第⼆个终端运⾏ vmstat , 观察上下⽂切换情况:
# 每隔1秒输出1组数据(需要Ctrl+C才结束)
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0
你应该可以发现, cs 列的上下⽂切换次数从之前的 35 骤然上升到了 139 万。 同时, 注意观察其他⼏个指标:
r 列:就绪队列的长度已经到了 8,远远超过了系统 CPU 的个数 2,所以肯定会有大量的 CPU 竞争。
us(user)和 sy(system)列:这两列的 CPU 使用率加起来上升到了 100%,其中系统 CPU 使用率,也就是 sy 列高达 84%,说明 CPU 主要是被内核占用了。
in 列:中断次数也上升到了 1 万左右,说明中断处理也是个潜在的问题。
综合这⼏个指标, 我们可以知道, 系统的就绪队列过⻓, 也就是正在运⾏和等待CPU的进程数过多, 导致了⼤量的上下⽂切换, ⽽上下⽂切换⼜导致了系统 CPU 的占⽤率升⾼。
那么到底是什么进程导致了这些问题呢?
我们继续分析, 在第三个终端再⽤ pidstat 来看⼀下, CPU 和进程上下⽂切换的情况:
# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
# -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
$ pidstat -w -u 1
08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2
08:06:33 UID PID cswch/s nvcswch/s Command
08:06:34 0 8 11.00 0.00 rcu_sched
08:06:34 0 16 1.00 0.00 ksoftirqd/1
08:06:34 0 471 1.00 0.00 hv_balloon
08:06:34 0 1230 1.00 0.00 iscsid
08:06:34 0 4089 1.00 0.00 kworker/1:5
08:06:34 0 4333 1.00 0.00 kworker/0:3
08:06:34 0 10499 1.00 224.00 pidstat
08:06:34 0 26326 236.00 0.00 kworker/u4:2
08:06:34 1000 26784 223.00 0.00 sshd
从pidstat的输出你可以发现, CPU 使⽤率的升⾼果然是 sysbench 导致的, 它的 CPU 使⽤率已经达到了 100%。 但上下⽂切换则是来⾃其他进程, 包括⾮⾃愿上下⽂切换频率最⾼的 pidstat , 以及⾃ 愿上下⽂切换频率最⾼的内核线程 kworker 和sshd。
不过, 细⼼的你肯定也发现了⼀个怪异的事⼉: pidstat 输出的上下⽂切换次数, 加起来也就⼏百, ⽐ vmstat 的 139 万明显⼩了太多。 这是怎么回事呢? 难道是⼯具本身出了错吗?
别着急, 在怀疑⼯具之前, 我们再来回想⼀下, 前⾯讲到的⼏种上下⽂切换场景。 其中有⼀点提到, Linux 调度的基本单位实际上是线程, ⽽我们的场景 sysbench 模拟的也是线程的调度问题, 那么, 是不是 pidstat 忽略了线程的数据呢?
通过运⾏ man pidstat , 你会发现, pidstat 默认显示进程的指标数据, 加上 -t 参数后, 才会输出线程的指标。
所以, 我们可以在第三个终端⾥, Ctrl+C 停⽌刚才的 pidstat 命令, 再加上 -t 参数, 重试⼀下看看:
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标
$ pidstat -wt 1
08:14:05 UID TGID TID cswch/s nvcswch/s Command
...
08:14:05 0 10551 - 6.00 0.00 sysbench
08:14:05 0 - 10551 6.00 0.00 |__sysbench
08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
08:14:05 0 - 10554 18827.00 103954.00 |__sysbench
...
现在你就能看到了, 虽然 sysbench 进程(也就是主线程) 的上下⽂切换次数看起来并不多, 但它的⼦线程的上下⽂切换次数却有很多。 看来, 上下⽂切换罪魁祸⾸, 还是过多的 sysbench 线程。
我们已经找到了上下⽂切换次数增多的根源, 那是不是到这⼉就可以结束了呢?
当然不是。 不知道你还记不记得, 前⾯在观察系统指标时, 除了上下⽂切换频率骤然升⾼, 还有⼀个指标也有很⼤的变化。 是的, 正是中断次数。 中断次数也上升到了1万, 但到底是什么类型的中断上升了, 现在还不清楚。 我们接下来继续抽丝剥茧找源头。
既然是中断, 我们都知道, 它只发⽣在内核态, ⽽ pidstat 只是⼀个进程的性能分析⼯具, 并不提供任何关于中断的详细信息, 怎样才能知道中断发⽣的类型呢?
没错, 那就是从 /proc/interrupts 这个只读⽂件中读取。 /proc 实际上是 Linux 的⼀个虚拟⽂件系统, ⽤于内核空间与⽤户空间之间的通信。 /proc/interrupts 就是这种通信机制的⼀部分, 提供了⼀个只读的中断使⽤情况。
在第三个终端⾥, Ctrl+C 停⽌刚才的 pidstat 命令, 然后运⾏下⾯的命令, 观察中断的变化情况:
# -d 参数表示高亮显示变化的区域
$ watch -d cat /proc/interrupts
CPU0 CPU1
...
RES: 2450431 5279697 Rescheduling interrupts
...
观察⼀段时间, 你可以发现, 变化速度最快的是重调度中断(RES) , 这个中断类型表示, 唤醒空闲状态的 CPU 来调度新的任务运⾏。 这是多处理器系统(SMP) 中, 调度器⽤来分散任务到不同 CPU 的机制, 通常也被称为处理器间中断(InterProcessor Interrupts, IPI) 。
所以, 这⾥的中断升⾼还是因为过多任务的调度问题, 跟前⾯上下⽂切换次数的分析结果是⼀致的。
通过这个案例, 你应该也发现了多⼯具、 多⽅⾯指标对⽐观测的好处。 如果最开始时, 我们只⽤了 pidstat 观测, 这些很严重的上下⽂切换线程, 压根⼉就发现不了了。
现在再回到最初的问题, 每秒上下⽂切换多少次才算正常呢?
这个数值其实取决于系统本身的 CPU 性能。 如果系统的上下⽂切换次数⽐较稳定, 那么从数百到⼀万以内, 都应该算是正常的。 但当上下⽂切换次数超过⼀万次, 或者切换次数出现数量级的增⻓时, 就很可能已经出现了性能问题。
这时, 你还需要根据上下⽂切换的类型, 再做具体分析。 ⽐⽅说:
- ⾃愿上下⽂切换变多了, 说明进程都在等待资源, 有可能发⽣了 I/O 等其他问题;
- ⾮⾃愿上下⽂切换变多了, 说明进程都在被强制调度, 也就是都在争抢 CPU, 说明 CPU 的确成了瓶颈;
- 中断次数变多了, 说明 CPU 被中断处理程序占⽤, 还需要通过查看 /proc/interrupts ⽂件来分析具体的中断类型。
5、⼩结
总结⼀下, 不管是哪种场景导致的上下⽂切换, 你都应该知道:
-
CPU 上下⽂切换, 是保证 Linux 系统正常⼯作的核⼼功能之⼀, ⼀般情况下不需要我们特别关注。
-
但过多的上下⽂切换, 会把CPU时间消耗在寄存器、 内核栈以及虚拟内存等数据的保存和恢复上, 从⽽缩短进程真正运⾏的时间, 导致系统的整体性能⼤幅下降。