读过倪朋飞的《Linux性能优化实战》经常说的 CPU 上下文切换是什么意思?一文。对CPU上下文切换有了少许了解。现总结如下。
1.什么是CPU上下文切换
上下文切换专业术语为Context Switch,我们可以参考Context Switch Definition一文。再这篇文章中,有如下定义:
上下文切换(有时也称为进程切换或任务切换):是指CPU从一个进程//线程切换到另一个进程/线程。
上下文是指任意时刻CPU寄存器和程序计数器的内容。
- 寄存器是CPU内部的一小部分非常快的内存(相对于CPU外部较慢的RAM主存),通常,在计算过程中它通过提供对常用值的快速访问来加快计算机程序的执行速度。
- 程序计数器是一种特殊的寄存器,它指示CPU在其指令序列中的位置,根据特定的系统,它保存着正在执行的指令的地址或下一条要执行的指令的地址。
如何理解上面这些概念呢,我们不妨看一下CPU的组成:
CPU由控制单元、计算单元和存储单元组成,在CPU内部通过内部总线连接。这三部分的作用如下:
- 控制单元这是整个CPU的控制中心,由指令寄存器IR(InstrucTIon Register)、指令译码器ID(InstrucTIon Decoder)和操作控制器OC(OperaTIon Controller)等部分构成。用来协调CPU执行指令的顺序。控制单元根据用户预先编好的程序,通过操作控制器OC,按确定的时序,向相应的部件发出操作控制信号。
- 运算单元这是运算的核心部分,可以执行算术运算和逻辑运算,运算单元按控制单元的命令进行运作,运算单元的全部操作都是由控制器的信号来控制,它只是执行部件。
- 存储单元包括CPU内部的缓存和寄存器组,是CPU中暂时存放数据的地方,用以保存待处理或者处理完成的数据。这是由于CPU访问寄存器的时间要大大短于访问内存的时间,采用寄存器,可以大大减少CPU访问内存的次数,从而提高CPU的效率。
CPU的工作原理图:
因此不难看出,CPU在执行任意指令的时候,都需要首先在寄存器和计数器中写入数据,之后才能执行指令。那么所谓的上下文切换,也就是说,只要CPU执行不同的指令,那么其寄存器和计数器中的内容都有可能不相同。而改变这些内容的过程,也是需要花费时间的,这就是所谓的上下文切换的过程。
2.CPU上下文切换的类型
导致CPU进行切换的场景,可以分为:
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
下面详细进行说明。
2.1 进程上下文切换
2.1.1 系统调用
首先需要说明一下系统调用,严格来讲,系统调用是指单个进程,由用户态切换倒内核态的过程,虽然这只发生在单个进程之内,但是CPU上下文切换是无法避免。
在Intel X86 处理器中,将系统分为4个特权级别,分别是RING0、RING1、RING2、RING3。RING0具有最高权限,依次到RING3具有最低的权限。Intel特权级别如下图:
通常我们的应用程序工作在RING3级别,而操作系统内核工作在RING0。RING3只能访问受限制的资源,不能直接访问内存等硬件设备,用户进程在执行的过程中,必须通过系统调用切换到内核的RING0中才能访问这些特权资源。
那么这句话可以理解为,实际上进程既可以在用户空间运行,又可以在内核空间运行,进行在用户空间运行,被成为进程的用户态,进程在内核空间运行,被称为进程的内核态。
而将进程从用户态切换到内核态,这种操作就成为系统调用。
我们在进行文件独写操作的过程中,就会多次进行系统调用。
系统调用必将导致CPU上下文切换,这个过程如下:
CPU保存用户态的指令位置,然后执行内核代码。
CPU更新内核代码的新位置,然后跳转到内核态执行内核任务。
等系统调用结束之后,CPU寄存器需要恢复之前的用户态内容,切换到用户态空间,继续运行程序。
因此,可以发现,一次系统调用的过程,实际上导致了CPU上下文两次切换。
2.1.2 进程切换
系统调用是在同一个进程中进行的,那么要是从一个进程切换到另外一个进程呢?
我们需要知道进程的切换的过程,通常来说,进程是由内核来进行管理和调度的,进程的切换只能发生在内核态。进程的上下文中,不仅仅包括了进程的虚拟内存、栈、全局变量等用户的空间资源,还包括了内核中的堆栈、寄存器和内核空间。
如果要进行切换,这也就意味着,在进行CPU上下文切换保存寄存器和程序计数器的数据之前,需要首先把该进程的虚拟内存,堆栈等保存下来,而在加载下一进程的时候,还需要将下一进程的这些数据刷新到内核。
如果进程的上下文切换次数过多,那么很容易导致CPU将大量的时间消耗在寄存器、内核栈、以及虚拟内存等资源在内核中的保存与恢复上。从而导致CPU真正运行的时间被压缩。这也是为什么当进程频繁切换也会导致load averages上升的原因。
此外,如果要刷新虚拟内存,那么管理虚拟内存的TLB也需要刷新,内存的处理将会变慢。尤其在多CPU的系统中,缓存被多个CPU共享,刷新虚拟缓存不仅影响当前处理器的进程,还可能会影响缓存的其他的处理器进程。
导致进程调度的场景有:
- 1.当前进程的CPU时间片耗尽,释放CPU,调度切换另外一个就绪的进程。
- 2.系统资源不足(如内存不足),要等到资源满足之后才能运行,此时也会挂起这个进程,执行其他的进程。
- 3.sleep方法导致自动挂起。
- 4.存在更高优先级的进程,为了确保高优先级进程运行,也会挂起由高优先级进程执行。
- 5.发生硬件中断,CPU上的进程会被中断挂起,来执行中断程序。
以上是导致进程切换问题的常见原因。
2.2 线程上下文切换
线程与进程的最大区别在于,线程是调度的基本单位,而进程则是资源分配的基本单位。在内核中的任务调度,实际上调度的是进程,而进程只是给线程提供了虚拟内存,全局变量等资源。实际上,所谓线程,也就是一个轻量级进程。
我们可以做如下理解:
- 当进程只有一个线程的时候,此时可以认为进程就等于线程。
- 当进程有多个线程的时候,这些线程会共享相统的虚拟内存和全局变量,这些资源在进行切换的时候不需要更换。
因此,线程的上下文切换可以分为两种情况:
- 前后两个线程属于不同的进程,此时等价于进程切换。
- 前后两个线程属于同一个线程,由于虚拟内存等资源共享,那么,只需要切换线程的私有数据寄存器等不共享的数据。
不难发现,线程相对于进程,在上下文切换中,消耗的资源更少,这也是线程的优势。
2.3 中断上下文切换
此外,在操作系统中,中断操作也会中断正常调度和执行的进程,来响应中断事件。在进行重点的过程中,需要将进程的当前状态保存,当中断结束之后,再从原来的运行状态中恢复。
与进程上下文切换的不同在于,中断的上下文切换不需要涉及进程的用户态,可以理解为一种临时的中断过程,因此只需要对进程的虚拟内存、全局变量等用户态资源进行保存和恢复。中断上下文中,只包括中断服务程序执行所必须的状态,包括CPU寄存器,内核堆栈,硬件中断参数。
由于中断处理比进程具有更高的优先级,所以中断上下文切换不会与进程的上下文切换同时发生。同理,由于中断会将正常运行的程序打断,因此大部分中断程序都不会太复杂,这样便于尽可能快的结束。
需要说明的是,虽然中断上下文切换不如进程上下文切换消耗大量的CPU,但是这个过程也是需要消耗资源的。再中断次数过多的时候,往往也需要进行关注,以免造成严重的性能问题。
3.如何查看系统中的上下文切换
3.1 vmstat
vmstat可以对系统中的上下文切换进行查看。
[root@m162p201 ~]# vmstat
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 0 1946188 0 1363404 0 0 0 6 0 0 2 1 97 0 0
vmstat的输出说明:
输出项 | 说明 |
---|---|
Procs | |
r: | (Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。 |
b: | (Blocked)则是处于不可中断睡眠状态的进程数。 |
Memory | |
swpd | 使用的虚拟内存的总量 |
free | 空闲内存的数量 |
buff | 使用的buffers的内存的数量 |
cache | 使用的caches内存的数量 |
inact | 非活动的内存量,使用-a参数显示 |
active | 活动的内存量,使用-a参数显示 |
Swap | |
si | 每秒从交换分区写入内存的大小。 |
so | 每秒从内存写入交换分区的大小。 |
IO | |
bi | 每秒从设备读取的块数量 |
bo | 每秒从设备写入的块数量 |
System | |
in | 每秒中断的次数。 |
cs | 每秒上下文切换的次数。 |
CPU | |
us | 运行非内核代码花费的时间。 |
sy | 运行内核代码花费的时间。 |
id | 空闲时间。 |
wa | 等待IO的时间。 |
st | 从虚拟机窃取的时间。 |
3.2 pidstat
vmstat 给出了系统总体的上下文切换情况。
而pidstat 的-w选项,可以查看每个进程的上下文切换的情况。
[root@m162p201 ~]# pidstat -w
Linux 3.10.0-514.el7.x86_64 (m162p201) 07/21/2021 _x86_64_ (2 CPU)
06:26:00 PM UID PID cswch/s nvcswch/s Command
06:26:00 PM 0 1 0.88 0.00 systemd
06:26:00 PM 0 2 0.00 0.00 kthreadd
06:26:00 PM 0 3 0.73 0.00 ksoftirqd/0
06:26:00 PM 0 7 0.01 0.00 migration/0
06:26:00 PM 0 8 0.00 0.00 rcu_bh
06:26:00 PM 0 9 41.54 0.00 rcu_sched
06:26:00 PM 0 10 0.25 0.00 watchdog/0
06:26:00 PM 0 11 0.25 0.00 watchdog/1
这个输出中最关键的两个指标:cswch/s 和nvcswch/s。
- cswch/s 表示每秒自愿上下文切换的次数。(voluntary context switches)
- nvcswch/s 表示每秒非自愿上下文切换的次数。(non voluntary context switches)
自愿上下文切换:指进程无法获得所需的资源,导致的上下文切换,如I/O,内存不足等情况。
非自愿上下文切换:指进程由于时间片已到,被系统强制调度,而发生的上下文切换。如大量进程都争抢CPU,这就很容易导致非自愿的上下文切换。
4.案例
[root@m162p201 ~]# sysbench --threads=10 --max-time=300 threads run
WARNING: --max-time is deprecated, use --time instead
sysbench 1.0.17 (using system LuaJIT 2.0.4)
Running the test with following options:
Number of threads: 10
Initializing random number generator from current time
Initializing worker threads...
Threads started!
通过vmstat查看:
[root@m162p201 ~]# 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
10 0 0 1936160 0 1363776 0 0 0 6 0 0 2 1 97 0 0
3 0 0 1936184 0 1363776 0 0 0 20 41275 1639600 8 83 9 0 1
7 0 0 1936184 0 1363776 0 0 0 0 34715 1754745 9 83 8 0 0
8 0 0 1936184 0 1363776 0 0 0 0 35482 1654496 11 84 6 0 0
7 0 0 1936184 0 1363776 0 0 0 0 35843 1498336 12 83 6 0 0
9 0 0 1936184 0 1363776 0 0 0 8 36941 1496857 14 80 4 0 2
6 0 0 1936184 0 1363776 0 0 0 0 30993 1553817 13 78 9 0 0
6 0 0 1936184 0 1363776 0 0 0 0 35074 1594656 12 72 5 0 11
7 0 0 1936184 0 1363776 0 0 0 0 33869 1595752 12 84 5 0 0
5 0 0 1936184 0 1363776 0 0 0 0 32162 1607547 12 80 8 0 0
5 0 0 1936184 0 1363776 0 0 0 8 38626 1846420 9 87 4 0 0
4 0 0 1936184 0 1363776 0 0 0 28 37519 1789715 11 83 7 0 0
4 0 0 1936184 0 1363776 0 0 0 0 37462 1801615 9 83 8 0 0
7 0 0 1936184 0 1363780 0 0 0 0 36605 1592458 11 83 6 0 1
6 0 0 1936184 0 1363780 0 0 0 0 33538 1619347 15 81 4 0 0
5 0 0 1936184 0 1363780 0 0 0 188 35578 1806499 9 85 6 0 0
7 0 0 1936184 0 1363780 0 0 0 15 33750 1830352 9 82 6 0 3
5 0 0 1936184 0 1363780 0 0 0 0 39891 1725509 9 85 7 0 0
10 0 0 1936184 0 1363780 0 0 0 0 40275 1747259 9 83 8 0 1
4 0 0 1936184 0 1363780 0 0 0 0 35405 1848040 9 84 7 0 0
7 0 0 1936184 0 1363780 0 0 0 8 36566 1732820 9 82 9 0 0
可以发现,每秒中断次数高达3万5千多次。而每秒的上下文切换次数高大180万次。而r列的就绪进程最高的时候达到10,远远超出了CPU核数。这说明正是正在运行和等待CPU的进程过多导致了上下文切换过高。
通过pidstat来分析具体情况:
[root@m162p201 ~]# pidstat -w -u 1
Linux 3.10.0-514.el7.x86_64 (m162p201) 07/21/2021 _x86_64_ (2 CPU)
07:38:43 PM UID PID %usr %system %guest %CPU CPU Command
07:38:44 PM 1008 8031 0.00 0.99 0.00 0.99 1 java
07:38:44 PM 0 8748 21.78 100.00 0.00 100.00 1 sysbench
07:38:43 PM UID PID cswch/s nvcswch/s Command
07:38:44 PM 0 1 0.99 0.00 systemd
07:38:44 PM 0 3 0.99 0.00 ksoftirqd/0
07:38:44 PM 0 9 23.76 0.00 rcu_sched
07:38:44 PM 0 13 0.99 0.00 ksoftirqd/1
07:38:44 PM 0 394 19.80 0.00 xfsaild/dm-0
07:38:44 PM 0 466 0.99 0.00 systemd-journal
07:38:44 PM 0 564 19.80 0.00 xfsaild/dm-2
07:38:44 PM 1000 2814 1.98 0.00 sshd
07:38:44 PM 0 7471 5.94 0.00 kworker/0:1
07:38:44 PM 0 8401 0.99 0.00 vmstat
07:38:44 PM 0 8766 0.99 0.00 pidstat
07:38:44 PM 0 17658 4.95 0.00 kworker/1:3
07:38:44 PM 0 18040 25.74 0.00 etcd
07:38:44 PM 0 18583 0.99 0.00 kworker/1:2
07:38:44 PM UID PID %usr %system %guest %CPU CPU Command
07:38:45 PM 0 8748 13.00 100.00 0.00 100.00 1 sysbench
07:38:44 PM UID PID cswch/s nvcswch/s Command
07:38:45 PM 0 1 1.00 0.00 systemd
07:38:45 PM 0 9 29.00 0.00 rcu_sched
07:38:45 PM 0 13 5.00 0.00 ksoftirqd/1
07:38:45 PM 0 394 20.00 0.00 xfsaild/dm-0
07:38:45 PM 0 564 20.00 0.00 xfsaild/dm-2
07:38:45 PM 1000 2814 2.00 0.00 sshd
07:38:45 PM 0 7471 6.00 0.00 kworker/0:1
07:38:45 PM 0 8401 1.00 0.00 vmstat
07:38:45 PM 0 8766 1.00 0.00 pidstat
07:38:45 PM 0 17658 4.00 0.00 kworker/1:3
07:38:45 PM 0 18040 35.00 0.00 etcd
可以看到sysbench的CPU使用率高达100%,上下文切换来自sshd。
但是需要注意的是,此时pidstat输出的是进程级别的。而此时我们模拟的场景是多线程。加上-t参数可以查看线程的情况。
此时得到的结果如下:
07:43:09 PM UID TGID TID cswch/s nvcswch/s Command
07:43:10 PM 0 - 8401 1.00 0.00 |__vmstat
07:43:10 PM 0 - 8749 18296.00 152756.00 |__sysbench
07:43:10 PM 0 - 8750 20518.00 138188.00 |__sysbench
07:43:10 PM 0 - 8751 21792.00 178550.00 |__sysbench
07:43:10 PM 0 - 8752 13193.00 185192.00 |__sysbench
07:43:10 PM 0 - 8753 17778.00 129568.00 |__sysbench
07:43:10 PM 0 - 8754 16804.00 140940.00 |__sysbench
07:43:10 PM 0 - 8755 12503.00 189608.00 |__sysbench
07:43:10 PM 0 - 8756 13254.00 156408.00 |__sysbench
07:43:10 PM 0 - 8757 23788.00 161097.00 |__sysbench
07:43:10 PM 0 - 8758 17234.00 184198.00 |__sysbench
sysbench 线程是造成上下文切换的主要原因。
此外,对于中断次数的上升,还可以通过如下命令:
// -d 参数表示高亮显示变化的区域
[root@m162p201 ~]# watch -d cat /proc/interrupts
Every 2.0s: cat /proc/interrupts Wed Jul 21 19:46:33 2021
CPU0 CPU1
0: 128 0 IO-APIC-edge timer
1: 2 548 IO-APIC-edge i8042
6: 0 3 IO-APIC-edge floppy
8: 0 0 IO-APIC-edge rtc0
9: 0 0 IO-APIC-fasteoi acpi
10: 0 2 IO-APIC-fasteoi virtio2
11: 12 14 IO-APIC-fasteoi ehci_hcd:usb1, uhci_hcd:usb2, uhci_hcd:usb3, uhci_hcd:usb4
12: 0 15 IO-APIC-edge i8042
14: 0 0 IO-APIC-edge ata_piix
15: 39029735 14085966 IO-APIC-edge ata_piix
24: 0 0 PCI-MSI-edge virtio1-config
25: 88089554 42795647 PCI-MSI-edge virtio1-req.0
26: 0 0 PCI-MSI-edge virtio0-config
27: 4100189961 2608 PCI-MSI-edge virtio0-input.0
28: 49663 60203 PCI-MSI-edge virtio0-output.0
NMI: 0 0 Non-maskable interrupts
LOC: 4023315197 637379664 Local timer interrupts
SPU: 0 0 Spurious interrupts
PMI: 0 0 Performance monitoring interrupts
IWI: 306928365 325429883 IRQ work interrupts
RTR: 0 0 APIC ICR read retries
RES: 331159832 126609215 Rescheduling interrupts
CAL: 22071136 48351098 Function call interrupts
TLB: 19949038 19533818 TLB shootdowns
TRM: 0 0 Thermal event interrupts
THR: 0 0 Threshold APIC interrupts
DFR: 0 0 Deferred Error APIC interrupts
MCE: 0 0 Machine check exceptions
MCP: 181302 181302 Machine check polls
ERR: 0
MIS: 0
PIN: 0 0 Posted-interrupt notification event
PIW: 0 0 Posted-interrupt wakeup event
重调度中断(RES)是变化最快的。这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。
6.总结
本文学习了CPU上下文切换的概念以及中断的类型。在日日常开发过程中,当遇到上下文切换次数过多的问题时,我们可以借助 vmstat 、 pidstat 和 /proc/interrupts等工具,来辅助排查问题的原因。
- 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
- 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
- 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。