在计算机中,上下文切换是指存储进程或线程的状态,以便以后可以还原它并从同一点恢复执行。这允许多个进程共享一个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 上下文切换。不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。

查看系统上下文切换

我们通常使用vmstatpidstat来查看系统上下文切换,先看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 时,就容易发生非自愿上下文切换。

实战案例

预先安装 sysbenchsysstat 包:

$ 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,ussyCPU 使用率加起来上升到了 100%,中断in列也有2000多,看起来也是个问题。

那么到底是什么进程导致了这些问题呢?我们在第三个和第四个终端分别打开toppidstat

$ 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

toppidstat都可以看出CPU 使用率的升高果然是 sysbench 导致的,它的 CPU 使用率已经达到了 100%。另外可以看出sysbench的子线程的上下文切换次数非常多。
最后我们还要在新的中断查看下中断次数:

# -d 参数表示高亮显示变化的区域
$ watch -d cat /proc/interrupts
            CPU0       CPU1
 RES:     982564    1036295   Rescheduling interrupts

观察一段时间,你可以发现,变化速度最快的是重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制。所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。

小结

现在再回到最初的问题,每秒上下文切换多少次才算正常呢?这个要看具体情况:如果系统的上下文切换次数比较稳定,那么从数百到一万以内,都应该算是正常的。但当上下文切换次数超过一万次,就可能已经出现了性能问题,这个时候我们可以借助 vmstatpidstat/proc/interrupts 等工具,来辅助排查性能问题的根源。