一些基础概念
我们知道,Linux 是一个多任务操作系统,它支持远大于 cpu 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 cpu 在很短的时间内,将 cpu 轮流分配给他们,造成多任务同时运行的错觉。
而每个任务运行前,cpu 都需要知道任务从哪里加载,又从哪里开始运行,也就是需要系统事先帮它设置好 cpu 寄存器和程序计数器,它们都是 cpu 在运行任务前,必须依赖的环境,因此也被叫做cpu 上下文。
上下文切换的分类
进程上下文切换
进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程上下文不仅包括了虚拟内存,栈,全局变量等用户空间的资源,还包括了内核堆栈,寄存器等内核空间的状态。
进程上下文切换的场景:
- 划分给当前进程的 cpu 时间片耗尽
- 进程所需系统资源(比如内存)不足的情况下挂起,等待系统资源就绪。
- 进程主动挂起,比如 sleep 函数。
- 更高优先级的任务抢占。
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
线程和进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位,所以:
- 当进程只有一个线程时,可以认为进程就等于线程。
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
- 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
这样一来,线程上下文切换可以分为以下两种情况: - 前后两个线程属于不通进程。此时,资源不共享,所以切换过程等同于进程切换
- 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以切换的时候,只需要切换线程的私有数据、寄存器等不共享的数据。
由此可以发现,同为上下文切换,但是进城内的上下文切换,性能要更好一些,这也就是多线程代替多进程的一个优势。
中断上下文切换
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
怎么分析上下文切换的问题
常用工具
vmstat是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 cpu 上下文切换和中断的次数。
比如,下面的输出就一个vmstat的使用示例:
vmstat 5
# 每隔 5 秒输出一组数据
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 1731492 562572 19648488 0 0 1 24 0 0 2 1 97 0 0
0 0 0 1734320 562572 19648612 0 0 0 246 16499 23450 2 1 97 0 0
0 0 0 1737196 562572 19648664 0 0 0 239 17506 24438 2 1 97 0 0
1 1 0 1736132 562572 19648636 0 0 0 199 17109 24104 2 1 97 0 0
这里,需要重点关注下一下四列内容:
- cs(context switch)是每秒上下文切换的次数。
- in(interrupt)是每秒中断的次数。
- r(Running or Runnable)是就绪队列的长度,就是正在运行和等待 cpu 的进程数,如果远大于 cpu 核心数,则需要重点关注。
- b(Blocked)则是处于不可中断睡眠状态的进程数。
vmstat只给出了系统总体的上下文切换情况,想要查看每个进程的详细情况,就需要使用pidstat工具了,给它加上-w选项,就可以查看每个进程上下文切换的情况了。
比如:
# 每隔 5 秒输出一组数据
pidstat -w 5
08:12:03 AM UID PID cswch/s nvcswch/s Command
08:12:08 AM 0 1 2.59 0.00 systemd
08:12:08 AM 0 8 0.40 0.00 kworker/0:1H-events_highpri
08:12:08 AM 0 10 3.59 0.00 ksoftirqd/0
08:12:08 AM 0 11 201.80 0.00 rcu_sched
这里需要重点关注以下两个对象:
- cswch,表示每秒自愿上下文切换(voluntary context switches)的次数。
- nvswch,表示每秒非自愿(non voluntary context switches)上下文切换的次数。
两个概念: - 所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
- 而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
案例分析
这里有一台 2c4g 的机器,安装了sysbench,sysstat工具。我们按照以下步骤来实际模拟下多线程调度切换的情况。
先看下空闲系统上下文切换情况:
# 间隔 1s 后输出一组数据
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 2590888 40356 864544 0 0 406 1166 340 464 9 2 88 0 0
可以看到,现在的上下文切换次数数是464 次每秒,中断次数是 340 次每秒,r和 b 都是 0。
接下来,我们模拟系统多线程调度的瓶颈。
# 以 10 个线程运行 5 分钟的基准测试,模拟多线程切换的问题
sysbench --threads=10 --max-time=300 threads run
WARNING: --max-time is deprecated, use --time instead
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 10
Initializing random number generator from current time
Initializing worker threads...
Threads started!
使用 vmstat 观察上下文切换情况
# 每隔 1s输出一组数据
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
8 0 0 2352000 47592 940192 0 0 232 642 1417 6273 8 7 85 0 0
8 0 0 2351992 47624 940192 0 0 0 328 24708 989038 34 66 1 0 0
8 0 0 2351960 47624 940192 0 0 0 0 28126 1007646 36 64 1 0 0
7 0 0 2351960 47624 940192 0 0 0 0 25801 1010638 33 67 1 0 0
10 0 0 2351960 47624 940192 0 0 0 0 19873 1037725 34 65 1 0 0
8 0 0 2351960 47624 940192 0 0 0 0 21823 1039407 35 65 1 0 0
6 0 0 2351960 47632 940196 0 0 0 64 24975 1004337 35 65 1 0 0
7 0 0 2351960 47632 940196 0 0 0 0 24334 1020564 36 63 1 0 0
7 0 0 2351992 47632 940196 0 0 0 0 27219 981063 34 65 1 0 0
由以上数据可以看到,cs 列的上下文切换此时从空闲的 464 上升到 100w 次左右,同时:
- r列:就绪队列长度在 8-10 之间,远超过系统 2 个 cpu 的个数,肯定会有大量的 cpu 竞争。
- us(user)和 sy(system)列:这两列的 CPU 使用率加起来上升到了 100%,其中系统 CPU 使用率,也就是 sy 列高达 65%,说明 CPU 大部分是被内核占用了。
- in 列:中断次数也上升到了 2 万左右,说明中断处理也是个潜在的问题。
综合这几个指标我们可以知道,系统的就绪对列过长,也就是正在运行和等待运行的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 cpu 使用率升高。
那么,到底是哪个进程导致的呢?我们继续通过pidstat工具来排查下
pidstat -w -u 1
Linux 5.4.119-19.0009.28 (VM-13-158-tencentos) 12/31/2023 _x86_64_ (2 CPU)
08:53:00 AM UID PID %usr %system %guest %wait %CPU CPU Command
08:53:01 AM 0 1 0.00 0.95 0.00 0.00 0.95 0 systemd
08:53:01 AM 0 40473 60.95 118.10 0.00 0.00 179.05 1 sysbench
08:53:00 AM UID PID cswch/s nvcswch/s Command
08:53:01 AM 0 1 0.95 0.00 systemd
08:53:01 AM 0 9 0.95 0.00 ksoftirqd/0
08:53:01 AM 0 10 148.57 0.00 rcu_sched
08:53:01 AM 0 37780 2.86 0.00 kworker/1:0-memcg_kmem_cache
08:53:01 AM 0 40525 0.95 10.48 pidstat
从 pidstat 输出可以看到,cpu 使用率升高果然是 sysbench 导致的,它的 cpu 使用率已经达到了 179%。
不过,上面显示的是进程上下文切换的次数,但是我们模拟的是线程切换次数,并且,这里显示的数据也和 vmstat 的输出相差甚远,查询 pidstat 手册之后发现,加上-t之后,才会输出线程相关的数据。我们加上-t选项看下:
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标
10:01:54 AM 0 - 67134 15639.62 82800.00 |__sysbench
10:01:54 AM 0 - 67135 17024.53 82316.98 |__sysbench
10:01:54 AM 0 - 67136 22240.57 77202.83 |__sysbench
10:01:54 AM 0 - 67137 15895.28 82843.40 |__sysbench
10:01:54 AM 0 - 67138 22740.57 79833.02 |__sysbench
10:01:54 AM 0 - 67139 14195.28 91814.15 |__sysbench
10:01:54 AM 0 - 67140 19863.21 84233.96 |__sysbench
10:01:54 AM 0 - 67141 17538.68 81353.77 |__sysbench
10:01:54 AM 0 - 67142 14560.38 80265.09 |__sysbench
10:01:54 AM 0 - 67143 19767.92 72928.30 |__sysbench
从上面的数据来看,虽然 sysbench进程的上下文切换次数不多,但是,它的子线程切换却有很多,所以,上下文切换的罪魁祸首,就是 sysbench 线程了。
还有一个问题也别忘记,我们用 vmstat 看的时候,发现中断次数也上升到了 2w 多,那中断的信息要怎么查看呢,答案是从/proc/interrupts文件中读取:
# -d 参数表示高亮显示变化的区域
watch -d cat /proc/interrupts
CPU0 CPU1
...
LOC: 8239839 9176016 Local timer interrupts
...
RES: 17266140 17259862 Rescheduling interrupts
...
观察一段时间,你可以发现,变化速度最快的是重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。其次为 时间中断(LOC),也是因为进程时间片用完,被系统强制中断。
综合以上工具,我们可以系统的分析上下文切换和 cpu 负载的关系,比如:
- 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
- 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
- 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。