如何理解上下文切换
Linux 是一个多任务操作系统,它支持远大于 CPU 数量的任务同时运行,这是通过频繁的上下文切换、将CPU轮流分配给不同任务从而实现的。
CPU 上下文切换,就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载 新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务,而保存下来的上下文, 会存储在系统内核中,并在任务重新调度执行时再次加载进来。
上下文切换的时机
- 根据调度策略,将CPU时间划片为对应的时间片,当时间片耗尽,就需要进行上下文切换
- 进程在系统资源不足,会在获取到足够资源之前进程挂起
- 进程通过sleep函数将自己挂起
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行, 也就是被抢占
- 当发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
上下文切换分类
我们之前讲过的任务到底是什么呢?
- 进程和线程是最常见的任务
- 硬件通过触发信号,会导致中断处理程序的调用,也是一种常见的任务
所以,根据任务的不同,CPU 的上下文切换可以分为不同的场景
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换
系统调用
Linux 按照特权等级划分进程的运行空间
- **内核空间(Ring 0):**具有最高权限,可以直接访问所有资源
- **用户空间(Ring 3):**只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内 核空间的时候,被称为进程的内核态。 从用户态到内核态的转变,需要通过系统调用来完成。
系统调用举例:
当我们查看文件内容时, 需要多次系统调用来完成:
- 首先调用 open() 打开文件,
- 然后调用 read() 读取文件内容,
- 并调用 write() 将内容写到标准输出,
- 最后再调用 close() 关闭文件。
系统调用的过程有没有发生 CPU 上下文的切换呢?答案自然是肯定的
- CPU 寄存器里原来用户态的指令位置,需要先保存起来
- 为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置
- 最后才是跳转到内核态运行内核任务
- 系统调用结束后,CPU 寄存器需要恢复原来保存的用户态
- 然后再切换回用户空间,继续运行进程
**系统调用和进程上下文切换的不同 **
- **进程上下文切换:**从一个进程切换到另一个进程运行
- **系统调用:**一直是同一个进程在运行
- 系统调用过程通常称为特权模式切换,而不是上下文切换
- 系统调用过程中, CPU 上下文切换是无法避免的
进程上下文切换
在 Linux 中,进程是由内核来管理和调度进程的,切换只能发生在内核态,进程的上下文不仅包括了 虚拟内存 、栈 、全局变量 等用户空间的资源,还包括了 内核堆栈、寄存器 等内核空间的资源。
**进程上下文切换: **
- 在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来【保存上下文**】 **
- 而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈【加载上下文】
什么时候会切换进程上下文
- 顾名思义,只有在进程切换时才需要切换上下文
- 换句话说,只有在进程调度时才需要切换上下文
CPU 如何挑选进程来运行?
- Linux 为每个 CPU 都维护了一个等待队列 ;
- 将活跃进程(正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序 ;
- 然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。
**进程上下文切换如何影响系统性能 **
每次上下文切换都需要几十纳秒到数微秒的 CPU 时间。这个时间还是相当可观的,特别是在进程上下文切换次数较多的情况下,很容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,进而大大缩短了真正运行进程的时间。这也正是上一节中我们所讲的,导致平均负载升高的一个重要因素。
线程上下文切换
线程是调度的基本单位,而进程则是资源拥有的基本单位。 所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
线程的上下文切换其实就可以分为两种情况:
第一种:前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
第二种:前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
中断上下文切换
硬件通过触发信号,向CPU发送中断信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核, 内核通过这些参数进行中断处理。 中断处理会打断进程的正常调度和执行,而在打 断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
例如:A进程启动写磁盘操作,A进程睡眠后B进程在运行,当磁盘写完后磁盘中断信号打断的是B进程,在中断处理时会唤醒A进程。
进程上下文 VS 中断上下文
- 内核可以处于两种上下文:进程上下文和中断上下文;
- 即便中断过程打断了 一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源;
- 中断上下文,只包括内核态中断服务程序执行所必需的状态,包括CPU 寄存器、内核堆栈、硬件中断参数;
- 中断上下文不会和进程上下文切换同时发生;
- 对同一个 CPU 来说,中断处理比进程拥有更高的优先级;
- 由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
耗资源程度
- 跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能;
- 当发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
查看上下文切换
如何来查看上下文切换呢?我们可以使用 vmstat
这个工具,来查询系统的上下文切换情况 。
vmstat
是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析CPU上下文切换和中断的次数 。
1)系统总的上下文切换情况
#vmstat n n秒后输出一行信息
[root@centos7-2 ~]# 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
1 0 0 299304 2108 155296 0 0 362 29 181 210 1 4 95 0 0
0 0 0 299288 2108 155296 0 0 0 0 187 165 1 0 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 173 157 0 0 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 165 160 0 1 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 181 169 0 0 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 181 162 0 1 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 150 155 0 0 100 0 0
0 0 0 299288 2108 155296 0 0 0 0 150 158 0 1 100 0 0
重点强调下,需要特别关注的四列内容:
cs(context switch)
是每秒上下文切换的次数。
in(interrupt)
则是每秒中断的次数。
r(Running or Runnable)
是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
b(Blocked)
则是处于不可中断睡眠状态的进程数。
vmstat
只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,就需要使用我们前面提到过的 pidstat了。给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了。
pidstat [ 选项 ] [ <时间间隔> ] [ <次数> ]
-u:默认的参数,显示各个进程的cpu使用统计
-r:显示各个进程的内存使用统计
-d:显示各个进程的IO使用情况
-p:指定进程号
-w:显示每个进程的上下文切换情况
-t:显示选择任务的线程的统计信息外的额外信息
[root@centos7-2 ~]# pidstat -w -u 3
Linux 3.10.0-693.el7.x86_64 (centos7-2) 2020年11月27日 _x86_64_ (2 CPU)
14时53分11秒 UID PID %usr %system %guest %wait %CPU CPU Command
14时53分14秒 0 1375 0.33 0.33 0.00 0.00 0.66 0 pidstat
14时53分11秒 UID PID cswch/s nvcswch/s Command
14时53分14秒 0 3 0.33 0.00 ksoftirqd/0
14时53分14秒 0 6 1.32 0.00 kworker/u256:0
14时53分14秒 0 9 4.64 0.00 rcu_sched
14时53分14秒 0 10 0.33 0.00 watchdog/0
14时53分14秒 0 11 0.33 0.00 watchdog/1
14时53分14秒 0 13 0.33 0.00 ksoftirqd/1
14时53分14秒 0 14 0.99 0.00 kworker/1:0
14时53分14秒 0 46 0.33 0.00 kworker/0:2
14时53分14秒 0 409 19.87 0.00 xfsaild/dm-0
14时53分14秒 0 654 10.26 0.00 vmtoolsd
14时53分14秒 0 720 0.33 0.00 kworker/1:1H
14时53分14秒 89 1175 0.33 0.00 qmgr
14时53分14秒 89 1274 0.33 0.00 cleanup
14时53分14秒 89 1280 0.99 0.00 trivial-rewrite
14时53分14秒 0 1305 0.33 0.00 sshd
14时53分14秒 0 1348 2.32 0.00 kworker/0:3
这个结果中有两列内容是我们的重点关注对象
这个结果中有两列内容是我们的重点关注对象。
一个是 cswch ,表示每秒自愿上下文切换(voluntary context switches)的次数,另一个则是 nvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数。
- 所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
- 而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢CPU时,就容易发生非自愿上下文切换。