在计算机中,上下文切换是指存储进程或线程的状态,以便以后可以还原它并从同一点恢复执行。这允许多个进程共享一个CPU
,这是多任务操作系统的基本功能。
Linux 是一个多任务操作系统,它支持远大于 CPU
数量的任务同时运行,这依赖于CPU
上下文切换。CPU
上下文切换,就是先把前一个任务的 CPU
上下文(也就是 CPU
寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务;而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
Linux上下文切换有三种潜在的触发条件:多任务上下文切换、中断处理上下文切换以及用户和内核模式切换。
多任务
最常见的是,在某些调度方案中,必须将一个进程从CPU
中切换出来,以便另一个进程可以运行。可以通过使自身无法运行的过程来触发此上下文切换,例如,等待I/O
或同步操作完成。在抢先式多任务系统上,调度程序还可以切换出仍可运行的进程。Linux 为每个 CPU
都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU
的进程)按照优先级和等待 CPU
的时间排序,然后选择最需要 CPU
的进程,也就是优先级最高和等待 CPU
时间最长的进程来运行。
多任务中,除了进程上下文切换,还包括线程上下文切换,线程与进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位。说白了,所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源;另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。如果前后两个线程属于不同进程,此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。如果前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
中断处理
现代架构是中断驱动的。这意味着,例如,如果CPU
从磁盘请求数据,则不需要忙于等待读取结束。它可以发出请求并继续执行其他操作。读取结束后,可以中断 CPU 并显示读取内容。对于中断,将安装一个称为中断处理程序的程序,该中断处理程序将处理来自磁盘的中断。
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
对同一个 CPU
来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU
,切换次数过多也会耗费大量的 CPU
,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
用户和内核模式切换
Linux 按照特权等级,把进程的运行空间分为内核空间(Ring 0
)和(Ring 3
),进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态:
- 内核空间(
Ring 0
)具有最高权限,可以直接访问所有资源; - 用户空间(
Ring 3
)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
当操作系统需要在用户模式和内核模式之间转换时,不需要上下文切换;模式转换本身并不是上下文切换。但是,在Linux
系统中,从用户态到内核态的转变,需要通过系统调用来完成。CPU
寄存器里原来用户态的指令位置,需要先保存起来,接着,为了执行内核态代码,CPU
寄存器需要更新为内核态指令的新位置,最后才是跳转到内核态运行内核任务。而系统调用结束后,CPU
寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU
上下文切换。不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。
查看系统上下文切换
我们通常使用vmstat
和pidstat
来查看系统上下文切换,先看vmstat
:
#每隔2秒输出一组数据,一共输出5组
$ vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 524 2455820 206824 3795240 0 0 14 148 78 153 1 1 98 0 0
0 0 524 2455820 206828 3795272 0 0 0 22 262 488 0 0 99 0 0
1 0 524 2455820 206828 3795276 0 0 0 0 252 472 0 0 100 0 0
0 0 524 2455820 206828 3795276 0 0 0 0 249 482 0 0 100 0 0
0 0 524 2455820 206828 3795276 0 0 0 0 245 469 0 1 99 0 0
重点关注如下几组数据:
cs(context switch)每秒上下文切换次数。
in(interrupt)每秒中断次数。
r(Running or Runnable)正在运行和等待 CPU 的进程数量。
b(Blocked)处于不可中断状态的进程数量。
再看pidstat
:
$ pidstat -w 3
Linux 5.0.0-32-generic (ubuntu) 10/26/2019 _x86_64_ (2 CPU)
09:44:58 AM UID PID cswch/s nvcswch/s Command
09:45:01 AM 0 9 0.33 0.00 ksoftirqd/0
09:45:01 AM 0 10 7.28 0.00 rcu_sched
重点关注如下两组数据:
cswch:每秒自愿上下文切换(voluntary context switches)的次数。自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
nvcswch:表示每秒非自愿上下文切换(non voluntary context switches)的次数。非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
实战案例
预先安装 sysbench
和 sysstat
包:
$ sudo apt install sysstat sysbench
然后先打开一个终端运行sysbench
:
# 以 5 个线程运行 10 分钟的基准测试,模拟多线程切换的问题
$ sysbench --threads=5 --max-time=600 threads run
在第二个终端打开vmstat
查看上下文切换:
$ vmstat 2 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
5 0 524 2447664 207216 3796324 0 0 14 144 81 317 1 1 98 0 0
5 0 524 2447656 207216 3796324 0 0 0 0 1485 1983261 21 79 0 0 0
5 0 524 2447656 207216 3796324 0 0 0 0 3421 1883889 18 82 0 0 0
5 0 524 2446032 207220 3796368 0 0 0 32 1973 1909547 20 79 1 0 0
5 0 524 2446032 207220 3796368 0 0 0 0 2232 1982718 23 78 0 0 0
你应该可以发现,cs
列的上下文切换次数骤然上升到了 190 万,就绪队列r
的长度已经到了 5,us
和 sy
列CPU
使用率加起来上升到了 100%,中断in
列也有2000多,看起来也是个问题。
那么到底是什么进程导致了这些问题呢?我们在第三个和第四个终端分别打开top
和pidstat
:
$ top
top - 10:12:14 up 17:35, 5 users, load average: 5.19, 3.35, 1.48
Tasks: 331 total, 1 running, 261 sleeping, 0 stopped, 0 zombie
%Cpu(s): 21.6 us, 78.3 sy, 0.0 ni, 0.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8140984 total, 2446660 free, 1690512 used, 4003812 buff/cache
KiB Swap: 2097148 total, 2096624 free, 524 used. 6125044 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
24329 simplei+ 20 0 119440 8120 6544 S 198.0 0.1 8:11.31 sysbench
24354 simplei+ 20 0 51452 4224 3396 R 0.7 0.1 0:00.10 top
10 root 20 0 0 0 0 I 0.3 0.0 0:24.95 rcu_sched
# 每隔 1 秒输出 1 组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标,而 -u 参数则表示输出 CPU 使用指标
$ pidstat -wt -u 1
Average: UID TGID TID %usr %system %guest %wait %CPU CPU Command
Average: 1000 24329 - 39.91 100.00 0.00 0.00 100.00 - sysbench
Average: 1000 - 24330 8.45 29.11 0.00 57.75 37.56 - |__sysbench
Average: 1000 - 24331 7.51 28.17 0.00 57.75 35.68 - |__sysbench
Average: 1000 - 24332 6.10 29.58 0.00 58.22 35.68 - |__sysbench
Average: 1000 - 24333 8.45 28.64 0.00 58.22 37.09 - |__sysbench
Average: 1000 - 24334 8.92 28.64 0.00 56.34 37.56 - |__sysbench
Average: UID TGID TID cswch/s nvcswch/s Command
Average: 1000 - 24330 575.12 360744.60 |__sysbench
Average: 1000 - 24331 2152.11 334425.35 |__sysbench
Average: 1000 - 24332 1810.80 348483.10 |__sysbench
Average: 1000 - 24333 214.08 366587.79 |__sysbench
Average: 1000 - 24334 872.77 358510.80 |__sysbench
从top
和pidstat
都可以看出CPU
使用率的升高果然是 sysbench
导致的,它的 CPU
使用率已经达到了 100%。另外可以看出sysbench
的子线程的上下文切换次数非常多。
最后我们还要在新的中断查看下中断次数:
# -d 参数表示高亮显示变化的区域
$ watch -d cat /proc/interrupts
CPU0 CPU1
RES: 982564 1036295 Rescheduling interrupts
观察一段时间,你可以发现,变化速度最快的是重调度中断(RES
),这个中断类型表示,唤醒空闲状态的 CPU
来调度新的任务运行。这是多处理器系统(SMP
)中,调度器用来分散任务到不同 CPU
的机制。所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。
小结
现在再回到最初的问题,每秒上下文切换多少次才算正常呢?这个要看具体情况:如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,就可能已经出现了性能问题,这个时候我们可以借助 vmstat
、 pidstat
和 /proc/interrupts
等工具,来辅助排查性能问题的根源。