1. 基本介绍
1.1 CPU 寄存器和程序计数器
linux是一个多任务操作系统,它支持远大于CPU数量的任务同时运行。
但并不是字面意思的同时运行,而是因为系统在很短的时间内,将CPU轮流分配给它们,造成多任务同时运行的错觉。
而每个任务运行前,CPU都需要知道任务从哪里加载、又从哪里开始运行,也就是说,需要系统实现帮它设置好CPU 寄存器和程序计数器(Program Counter,PC)。
1.1.1 CPU寄存器
CPU寄存器,是CPU内置的容量小、速度极快的内存。
1.1.2 程序计数器
用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。
1.1.3 CPU上下文
就是CPU寄存器和程序计数器,它们都是CPU在运行任何任务前,必须依赖的环境,因此也被叫做CPU上下文。
1.2 CPU上下文切换
CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
2. CPU上下文切换的常见场景
2.1 进程上下文切换
2.1.1 用户态与内核态
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。
- 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
- 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
换个角度看,也就是说,进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。
从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
2.1.2 系统调用的过程
CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
不过,需要注意的是,系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。
- 进程上下文切换,是指从一个进程切换到另一个进程运行(内核态)。
- 而系统调用过程中一直是同一个进程在运行。
这跟我们通常所说的进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行。而系统调用过程中一直是同一个进程在运行。
所以,系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。
2.1.3 CPU就绪队列
Linux为每个cpu都维护了一个就绪队列,将活跃的进程(正在运行和正在等待cpu的进程)按照优先级和等待CPU的时长来排序,然后选择最需要CPU的进程,也就是优先级最高和等待CPU时间最长的进程来运行。
2.1.4 进程什么时候会被调度到cpu上运行
- CPU按照单位时间,被划分为一段段的时间片,这些时间片会被分配给各个进程,当A进程的时间片消耗完了,就会被系统挂起,切换到其他正在等待CPU的进程。
- A进程执行完了终止了,使用的CPU被释放,再从就绪队列中拿一个新的B进程来运行。
- 进程在系统资源不足(如内存)时,会被系统挂起,直到有资源可用才能继续运行,这个挂起的时间内系统会调度其他进程来运行。
- 进程通过睡眠函数sleep将自己主动挂起,也会触发重新调度。
- 当有优先级更高的进程要运行时,当前正在运行的低优先级的进程会被挂起,由高优先级的进程来执行。
- 发生硬中断时,cpu上的进程会被挂起,转而执行内核中的中断服务程序。
2.2 线程上下文切换
2.2.1 线程和进程的区别
线程是调度的基本单位,而进程是资源拥有的基本单位。
内核中的任务调度,实际上调度的是线程,进程只是给线程提供了虚拟内存、全局变量等资源。
所以可以这样理解线程和进程:
- 当进程只有一个线程时,可以认为进程就等于线程。
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
- 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
2.2.2 线程上下文切换的两种情况
- 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
- 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
2.2.3 线程上下文切换小结
同进程内的线程切换,要比多进程间的切换,消耗资源更少。这个特点也正是多线程代替多进程的一个优势。
2.3 中断上下文切换
中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。
打断进程这个过程,就要将当前进程的状态保存下来,待中断结束后,再恢复执行。
2.3.1 中断上下文切换和进程上下文切换的区别
中断上下文切换不涉及到进程的用户态,所以,就算中断过程中打断了一个正在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等资源。
中断上下文只包括内核态中断服务程序所必须的状态,包含CPU寄存器、内核堆核、硬件中断参数等。
对同一个CPU来说,中断处理比普通进程拥有更高的优先级,所以中断和普通进程上下文切换不会同时发生。
另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。
2.4 小结
- CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。
- 但过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。
3. 查看系统上下文切换情况
过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成了系统性能大幅下降的一个元凶。
可以使用 vmstat 这个工具,来查询系统的上下文切换情况。
3.1 vmstat
3.1.1 基本介绍
vmstat 是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 CPU 上下文切换和中断的次数以及I/O性能。
3.1.2 输出参数介绍
[root@VM-14-11-tencentos ~]# vmstat # -w number,可以每隔number秒输出一次
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
0 1 0 2615716 808196 14879080 0 0 5 11 0 0 2 1 97 0 0
- procs(进程)
r:就绪队列(正在运行和等待CPU运行)长度。此处为0,说明没有进程在等待CPU时间。
b:等待I/O的进程数(不可中断睡眠状态进程数)。此处为1,说明有1个进程正在等待I/O操作。- memory(内存)
swpd:使用的交换空间大小(单位通常是KB)。
free:空闲内存(单位通常是KB)。
buff:用作缓冲区的内存量(单位通常是KB)。
cache:用作缓存的内存量(单位通常是KB)。
incat:未活动的内存数量(-a选项)。
active:活动的内存数量(-a选项)。- swap(交换空间)
si:从磁盘交换的内存量(/s)。
so:交换到磁盘的内存量(/s)。- IO
bi:从块设备接收的块数(blocks/秒)。
bo:发送到块设备的块数(Blocks /s)。- system(系统)
in:每秒中断次数。
cs:每秒上下文切换次数。- CPU(总CPU时间的百分比)
us:用户空间(非内核代码)占用CPU时间百分比。
sy:系统空间(内核代码)占用CPU时间百分比。
id:CPU处于空闲时间百分比。
wa:CPU等待I/O操作完成的百分比。
st:虚拟机偷取的时间百分比。
3.2 pidstat(查看进程上下文切换情况)
vmstat只能给出系统整体的上下文切换情况,如果要查看每个进程的详细情况,则需要使用pidstat。
3.2.1 输出参数介绍
[root@VM-14-11-tencentos ~]# pidstat
Linux 5.4.119-19.0009.40 (VM-14-11-tencentos) 12/19/2024 _x86_64_ (16 CPU)
02:18:16 PM UID PID %usr %system %guest %CPU CPU Command
02:18:16 PM 0 1 0.03 0.02 0.00 0.05 14 systemd
……省略部分输出
[root@VM-14-11-tencentos ~]# pidstat -w 10 # 要查看进程上下文切换,一定要加-w参数
Linux 5.4.119-19.0009.40 (VM-14-11-tencentos) 12/19/2024 _x86_64_ (16 CPU)
02:40:10 PM UID PID cswch/s nvcswch/s Command
02:40:20 PM 0 1 0.90 0.00 systemd
- UID:用户ID,表示运行该进程的用户ID。
- PID:进程ID,表示进程的唯一标识符。
- %user:用户态CPU使用率。
- %system:系统态(内核态)CPU使用率。
- %guest:任务在虚拟机(运行虚拟处理器)中占用的CPU百分比。
- %CPU:总CPU使用率。
- CPU:显示该进程当前运行的CPU核心编号。
- Command:启动该进程的命令。
- cswch/s:每秒自愿上下文切换的次数。
- nvcswch/s:每秒非自愿上下文切换的次数。
3.2.2 cswch与nvcswch(重点关注)
- cswch/s:每秒自愿上下文切换的次数。
- nvcswch/s:每秒非自愿上下文切换的次数。
- 所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
- 而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
4. 案例分析
4.1 环境准备
机器配置:4C 8G
测试工具:安装 sysbench 和 sysstat 包。
操作系统:Centos 7
这里可以先运行一下vmstat,记录一下空闲系统的上下文切换次数
[root@VM-15-135-tencentos ~]# vmstat 1 1 # 间隔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 401208 248016 5037072 0 0 0 28 0 0 5 2 93 0 0
4.2 操作与分析
4.2.1 模拟多线程调度瓶颈(终端一)
在第一个中端中运行sysbench,模拟系统多线程调度瓶颈(注意sysbench模拟的是线程调度)
# 以10个线程运行5分钟的基准测试,模拟多线程切换的问题
[root@VM-15-135-tencentos ~]# 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!
4.2.2 vmstat观察上下文切换(终端二)
[root@VM-15-135-tencentos ~]# vmstat 2 # 每隔2s输出一次
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 0 379392 248020 5048900 0 0 0 28 0 1 5 2 93 0 0
5 0 0 378636 248020 5048900 0 0 0 18 18427 1136795 17 70 13 0 0
4 0 0 378300 248020 5048960 0 0 0 192 18195 1146202 21 68 11 0 0
5 0 0 377732 248020 5048996 0 0 0 22 15265 1161899 14 71 14 0 0
7 0 0 378992 248020 5049004 0 0 0 56 16277 1137969 20 68 12 0 0
7 0 0 376540 248020 5049008 0 0 0 46 17708 1166137 20 69 11 0 0
^C
从输出可以看到:
- cs列:上下文切换次数从原来的0直接上升到了一百多万。
- r列:就绪队列的长度最大已经到7了,这台服务器只有4c,所以有部分cpu开始竞争了。
- us(用户态)和sy(内核态)列:这两列加起来最大到了89%。其中sy列69,说明cpu主要是被内核占用。
- in列:中断次数也上升到了 1 万左右,说明中断处理也是个潜在的问题。
综合这几个指标,我们可以知道,系统的就绪队列过长,也就是正在运行和等待 CPU 的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 CPU 的占用率升高。
4.2.3 pidstat观察cpu与进程上下文切换(终端三)
[root@VM-15-135-tencentos ~]# pidstat -w -u 1
Linux 5.4.119-19.0009.40 (VM-15-135-tencentos) 12/19/2024 _x86_64_ (4 CPU)
06:08:40 PM UID PID %usr %system %guest %CPU CPU Command
06:08:41 PM 0 706 1.98 0.00 0.00 1.98 2 kubelet
06:08:41 PM 0 716 1.98 0.00 0.00 1.98 1 dockerd
06:08:41 PM 0 12918 0.00 0.99 0.00 0.99 0 loglistener
06:08:41 PM 1337 13581 2.97 0.00 0.00 2.97 3 envoy
06:08:41 PM 0 107949 0.99 0.99 0.00 1.98 2 YDService
06:08:41 PM 0 303089 1.98 0.00 0.00 1.98 3 cadvisor
06:08:41 PM 0 2557997 0.99 0.00 0.00 0.99 1 barad_agent
06:08:41 PM 0 3686633 48.51 292.08 0.00 340.59 3 sysbench
06:08:41 PM 0 3688012 0.00 0.99 0.00 0.99 3 pidstat
06:08:40 PM UID PID cswch/s nvcswch/s Command
06:08:41 PM 0 1 0.99 0.00 systemd
06:08:41 PM 0 9 19.80 0.00 ksoftirqd/0
06:08:41 PM 0 10 415.84 0.00 rcu_sched
06:08:41 PM 0 11 3.96 0.00 migration/0
06:08:41 PM 0 15 0.99 0.00 migration/1
06:08:41 PM 0 16 13.86 0.00 ksoftirqd/1
06:08:41 PM 0 20 0.99 0.00 migration/2
06:08:41 PM 0 21 16.83 0.00 ksoftirqd/2
06:08:41 PM 0 25 1.98 0.00 migration/3
06:08:41 PM 0 26 26.73 0.00 ksoftirqd/3
06:08:41 PM 0 700 4.95 0.00 rngd
06:08:41 PM 0 12016 3.96 0.00 monitor-agent
06:08:41 PM 0 12679 4.95 0.00 tke-bridge-agen
06:08:41 PM 0 12918 196.04 0.00 loglistener
06:08:41 PM 0 13286 1.98 0.00 fluentd
06:08:41 PM 0 13390 0.99 0.00 fluent-bit
06:08:41 PM 1337 13581 13.86 3.96 envoy
06:08:41 PM 0 13605 2.97 0.00 ruby
06:08:41 PM 0 108124 1.98 0.00 YDLive
06:08:41 PM 0 2557987 0.99 0.00 barad_agent
06:08:41 PM 0 3667293 8.91 0.00 kworker/2:0-events_power_efficient
06:08:41 PM 0 3673743 3.96 0.00 kworker/0:1-events
06:08:41 PM 0 3677386 1.98 0.00 kworker/3:0-events
06:08:41 PM 0 3684707 7.92 0.00 kworker/1:1-events
06:08:41 PM 0 3688012 0.99 0.99 pidstat
06:08:41 PM 0 3688015 3.96 6.93 sh
通过输出可以看到,CPU使用率升高就是sysbench导致的,但线程下上文切换则是来自其他进程。
但这里有一个很怪异的事,那就是pidstat的线程上下文切换次数,对比之前使用的vmstat显示的线程上下文切换次数,少了不是一点半点。
原因是因为sysbench模拟的是线程调度问题,而pidstat默认显示的则是进程的指标数据,如果要查看线程的指标,需要使用-t参数。
4.2.4 pidstat观察线程上下文切换(终端三)
pidstat -wt 查看线程指标数据
[root@VM-15-135-tencentos ~]# pidstat -wt 1|grep sysbench
11:09:20 AM UID TGID TID cswch/s nvcswch/s Comman
11:09:47 AM 0 - 1350019 33539.42 60121.15 |__sysbench
11:09:47 AM 0 - 1350020 34023.08 46891.35 |__sysbench
11:09:47 AM 0 - 1350021 34839.42 63647.12 |__sysbench
11:09:47 AM 0 - 1350022 33682.69 51033.65 |__sysbench
11:09:47 AM 0 - 1350023 35148.08 51330.77 |__sysbench
11:09:47 AM 0 - 1350024 35768.27 56367.31 |__sysbench
11:09:47 AM 0 - 1350025 34617.31 47892.31 |__sysbench
11:09:47 AM 0 - 1350026 35791.35 53427.88 |__sysbench
11:09:47 AM 0 - 1350027 31137.50 53968.27 |__sysbench
11:09:47 AM 0 - 1350028 36104.81 60716.35 |__sysbench
这样就可以看到,10个sysbench子线程,都有非常多线程上下文切换,也就是说上下文切换的罪魁祸首,还是过多的sysbench线程。
- cswch/s:自愿上下文切换
- 通常是因为进程自身的原因,比如进程主动放弃了 CPU 资源,常见的情况有进程在等待某些事件发生(例如等待 I/O 操作完成、等待定时器超时、等待获取某个锁等),此时它会主动让出 CPU,以便让其他进程可以使用。
- 较高的自愿上下文切换,会影响执行效率。
- nvcswch/s:非自愿上下文切换
- 指的是进程/线程被系统强制剥夺CPU使用权,进而切换到其他进程去执行。
- 一般发生在当前进程的时间片用完了(操作系统为了保证公平性和系统整体效率,会给每个进程分配一定的时间片来使用 CPU,时间片耗尽就会切换),或者有更高优先级的进程进入可运行状态需要立即使用 CPU 等场景下。
- 较高的非自愿上下文切换,可能是系统CPU资源比较紧张,或者存在高优先级进程频繁抢占CPU,也会影响程序和系统的运行效率。
4.2.5 查看系统中断情况(终端三)
之前用vmstat查看线程指标的时候,发现in列,中断次数也有一万多次,但是到底是什么类型的中断,不得而知。
由于中断只发生在内核态,所以只能从/proc/interrupts文件中读取。/proc是一个虚拟文件系统,用于内核空间和用户空间之间的通信,/proc/interrupts 就是这种通信机制的一部分,提供了一个只读的中断使用情况。
[root@VM-15-135-tencentos ~]# watch -d cat /proc/interrupts # -d 高亮显示变化部分
Every 2.0s: cat /proc/interrupts Fri Dec 20 11:32:26 2024
CPU0 CPU1 CPU2 CPU3
RES: 573959727 527846917 417013401 348337894 Rescheduling interrupts
通过持续观察,我这里变化最大的就是RES行。
RES(重调度中断),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。
所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。
4.3 每秒上下文切换多少次属于正常
主要取决于CPU性能,如果系统每秒的上下文切换次数比较稳定,那么每秒数百到一万以内,都算正常。如果超了,可能就有问题了。
这个时候,就需要根据上下文切换的类型,具体分析:
- 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
- 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
- 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。