CPU性能篇-CPU上下文切换与案例分析-Day 02

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上运行

  1. CPU按照单位时间,被划分为一段段的时间片,这些时间片会被分配给各个进程,当A进程的时间片消耗完了,就会被系统挂起,切换到其他正在等待CPU的进程。
  2. A进程执行完了终止了,使用的CPU被释放,再从就绪队列中拿一个新的B进程来运行。
  3. 进程在系统资源不足(如内存)时,会被系统挂起,直到有资源可用才能继续运行,这个挂起的时间内系统会调度其他进程来运行。
  4. 进程通过睡眠函数sleep将自己主动挂起,也会触发重新调度。
  5. 当有优先级更高的进程要运行时,当前正在运行的低优先级的进程会被挂起,由高优先级的进程来执行。
  6. 发生硬中断时,cpu上的进程会被挂起,转而执行内核中的中断服务程序。

2.2 线程上下文切换

2.2.1 线程和进程的区别

线程是调度的基本单位,而进程是资源拥有的基本单位。

内核中的任务调度,实际上调度的是线程,进程只是给线程提供了虚拟内存、全局变量等资源。

所以可以这样理解线程和进程:

  • 当进程只有一个线程时,可以认为进程就等于线程。
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
  • 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

2.2.2 线程上下文切换的两种情况

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
  2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

2.2.3 线程上下文切换小结

同进程内的线程切换,要比多进程间的切换,消耗资源更少。这个特点也正是多线程代替多进程的一个优势。

2.3 中断上下文切换

中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。
打断进程这个过程,就要将当前进程的状态保存下来,待中断结束后,再恢复执行。

2.3.1 中断上下文切换和进程上下文切换的区别

中断上下文切换不涉及到进程的用户态,所以,就算中断过程中打断了一个正在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等资源。

中断上下文只包括内核态中断服务程序所必须的状态,包含CPU寄存器、内核堆核、硬件中断参数等。

对同一个CPU来说,中断处理比普通进程拥有更高的优先级,所以中断和普通进程上下文切换不会同时发生。

另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。

2.4 小结

  1. CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。
  2. 但过多的上下文切换,会把 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
  1. procs(进程)
    r:就绪队列(正在运行和等待CPU运行)长度。此处为0,说明没有进程在等待CPU时间。
    b:等待I/O的进程数(不可中断睡眠状态进程数)。此处为1,说明有1个进程正在等待I/O操作。
  2. memory(内存)
    swpd:使用的交换空间大小(单位通常是KB)。
    free:空闲内存(单位通常是KB)。
    buff:用作缓冲区的内存量(单位通常是KB)。
    cache:用作缓存的内存量(单位通常是KB)。
    incat:未活动的内存数量(-a选项)。
    active:活动的内存数量(-a选项)。
  3. swap(交换空间)
    si:从磁盘交换的内存量(/s)。
    so:交换到磁盘的内存量(/s)。
  4. IO
    bi:从块设备接收的块数(blocks/秒)。
    bo:发送到块设备的块数(Blocks /s)。
  5. system(系统)
    in:每秒中断次数。
    cs:每秒上下文切换次数。
  6. 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
  1. UID:用户ID,表示运行该进程的用户ID。
  2. PID:进程ID,表示进程的唯一标识符。
  3. %user:用户态CPU使用率。
  4. %system:系统态(内核态)CPU使用率。
  5. %guest:任务在虚拟机(运行虚拟处理器)中占用的CPU百分比。
  6. %CPU:总CPU使用率。
  7. CPU:显示该进程当前运行的CPU核心编号。
  8. Command:启动该进程的命令。
  9. cswch/s:每秒自愿上下文切换的次数。
  10. nvcswch/s:每秒非自愿上下文切换的次数。

3.2.2 cswch与nvcswch(重点关注)

  1. cswch/s:每秒自愿上下文切换的次数。
  2. 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 文件来分析具体的中断类型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值