Linux 上下文切换分析

一些基础概念

我们知道,Linux 是一个多任务操作系统,它支持远大于 cpu 数量的任务同时运行。当然,这些任务实际上并不是真的在同时运行,而是因为系统在很短的时间内,将 cpu 在很短的时间内,将 cpu 轮流分配给他们,造成多任务同时运行的错觉。
而每个任务运行前,cpu 都需要知道任务从哪里加载,又从哪里开始运行,也就是需要系统事先帮它设置好 cpu 寄存器和程序计数器,它们都是 cpu 在运行任务前,必须依赖的环境,因此也被叫做cpu 上下文

上下文切换的分类

进程上下文切换

进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程上下文不仅包括了虚拟内存,栈,全局变量等用户空间的资源,还包括了内核堆栈,寄存器等内核空间的状态。
进程上下文切换的场景:

  • 划分给当前进程的 cpu 时间片耗尽
  • 进程所需系统资源(比如内存)不足的情况下挂起,等待系统资源就绪。
  • 进程主动挂起,比如 sleep 函数。
  • 更高优先级的任务抢占。
  • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换

线程和进程最大的区别在于,线程是调度的基本单位,而进程则是资源拥有的基本单位,所以:

  • 当进程只有一个线程时,可以认为进程就等于线程。
  • 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
  • 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
    这样一来,线程上下文切换可以分为以下两种情况:
  • 前后两个线程属于不通进程。此时,资源不共享,所以切换过程等同于进程切换
  • 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以切换的时候,只需要切换线程的私有数据、寄存器等不共享的数据。
    由此可以发现,同为上下文切换,但是进城内的上下文切换,性能要更好一些,这也就是多线程代替多进程的一个优势。
中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。

怎么分析上下文切换的问题

常用工具

vmstat是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况,也常用来分析 cpu 上下文切换和中断的次数。
比如,下面的输出就一个vmstat的使用示例:

vmstat 5
# 每隔 5 秒输出一组数据
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 1731492 562572 19648488    0    0     1    24    0    0  2  1 97  0  0
 0  0      0 1734320 562572 19648612    0    0     0   246 16499 23450  2  1 97  0  0
 0  0      0 1737196 562572 19648664    0    0     0   239 17506 24438  2  1 97  0  0
 1  1      0 1736132 562572 19648636    0    0     0   199 17109 24104  2  1 97  0  0

这里,需要重点关注下一下四列内容:

  • cs(context switch)是每秒上下文切换的次数。
  • in(interrupt)是每秒中断的次数。
  • r(Running or Runnable)是就绪队列的长度,就是正在运行和等待 cpu 的进程数,如果远大于 cpu 核心数,则需要重点关注。
  • b(Blocked)则是处于不可中断睡眠状态的进程数。

vmstat只给出了系统总体的上下文切换情况,想要查看每个进程的详细情况,就需要使用pidstat工具了,给它加上-w选项,就可以查看每个进程上下文切换的情况了。
比如:

# 每隔 5 秒输出一组数据
pidstat -w 5
08:12:03 AM   UID       PID   cswch/s nvcswch/s  Command
08:12:08 AM     0         1      2.59      0.00  systemd
08:12:08 AM     0         8      0.40      0.00  kworker/0:1H-events_highpri
08:12:08 AM     0        10      3.59      0.00  ksoftirqd/0
08:12:08 AM     0        11    201.80      0.00  rcu_sched

这里需要重点关注以下两个对象:

  • cswch,表示每秒自愿上下文切换(voluntary context switches)的次数。
  • nvswch,表示每秒非自愿(non voluntary context switches)上下文切换的次数。
    两个概念:
  • 所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
  • 而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

案例分析

这里有一台 2c4g 的机器,安装了sysbench,sysstat工具。我们按照以下步骤来实际模拟下多线程调度切换的情况。
先看下空闲系统上下文切换情况:

# 间隔 1s 后输出一组数据
 vmstat 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 2590888  40356 864544    0    0   406  1166  340  464  9  2 88  0  0

可以看到,现在的上下文切换次数数是464 次每秒,中断次数是 340 次每秒,r和 b 都是 0。
接下来,我们模拟系统多线程调度的瓶颈。

# 以 10 个线程运行 5 分钟的基准测试,模拟多线程切换的问题
sysbench --threads=10 --max-time=300 threads run
WARNING: --max-time is deprecated, use --time instead
sysbench 1.0.20 (using system LuaJIT 2.1.0-beta3)
Running the test with following options:
Number of threads: 10
Initializing random number generator from current time
Initializing worker threads...
Threads started!

使用 vmstat 观察上下文切换情况

# 每隔 1s输出一组数据
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
 8  0      0 2352000  47592 940192    0    0   232   642 1417 6273  8  7 85  0  0
 8  0      0 2351992  47624 940192    0    0     0   328 24708 989038 34 66  1  0  0
 8  0      0 2351960  47624 940192    0    0     0     0 28126 1007646 36 64  1  0  0
 7  0      0 2351960  47624 940192    0    0     0     0 25801 1010638 33 67  1  0  0
10  0      0 2351960  47624 940192    0    0     0     0 19873 1037725 34 65  1  0  0
 8  0      0 2351960  47624 940192    0    0     0     0 21823 1039407 35 65  1  0  0
 6  0      0 2351960  47632 940196    0    0     0    64 24975 1004337 35 65  1  0  0
 7  0      0 2351960  47632 940196    0    0     0     0 24334 1020564 36 63  1  0  0
 7  0      0 2351992  47632 940196    0    0     0     0 27219 981063 34 65  1  0  0

由以上数据可以看到,cs 列的上下文切换此时从空闲的 464 上升到 100w 次左右,同时:

  • r列:就绪队列长度在 8-10 之间,远超过系统 2 个 cpu 的个数,肯定会有大量的 cpu 竞争。
  • us(user)和 sy(system)列:这两列的 CPU 使用率加起来上升到了 100%,其中系统 CPU 使用率,也就是 sy 列高达 65%,说明 CPU 大部分是被内核占用了。
  • in 列:中断次数也上升到了 2 万左右,说明中断处理也是个潜在的问题。

综合这几个指标我们可以知道,系统的就绪对列过长,也就是正在运行和等待运行的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 cpu 使用率升高。

那么,到底是哪个进程导致的呢?我们继续通过pidstat工具来排查下

pidstat -w  -u 1
Linux 5.4.119-19.0009.28 (VM-13-158-tencentos)  12/31/2023  _x86_64_    (2 CPU)

08:53:00 AM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
08:53:01 AM     0         1    0.00    0.95    0.00    0.00    0.95     0  systemd
08:53:01 AM     0     40473   60.95  118.10    0.00    0.00  179.05     1  sysbench

08:53:00 AM   UID       PID   cswch/s nvcswch/s  Command
08:53:01 AM     0         1      0.95      0.00  systemd
08:53:01 AM     0         9      0.95      0.00  ksoftirqd/0
08:53:01 AM     0        10    148.57      0.00  rcu_sched
08:53:01 AM     0     37780      2.86      0.00  kworker/1:0-memcg_kmem_cache
08:53:01 AM     0     40525      0.95     10.48  pidstat

从 pidstat 输出可以看到,cpu 使用率升高果然是 sysbench 导致的,它的 cpu 使用率已经达到了 179%。

不过,上面显示的是进程上下文切换的次数,但是我们模拟的是线程切换次数,并且,这里显示的数据也和 vmstat 的输出相差甚远,查询 pidstat 手册之后发现,加上-t之后,才会输出线程相关的数据。我们加上-t选项看下:

# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标
10:01:54 AM     0         -     67134  15639.62  82800.00  |__sysbench
10:01:54 AM     0         -     67135  17024.53  82316.98  |__sysbench
10:01:54 AM     0         -     67136  22240.57  77202.83  |__sysbench
10:01:54 AM     0         -     67137  15895.28  82843.40  |__sysbench
10:01:54 AM     0         -     67138  22740.57  79833.02  |__sysbench
10:01:54 AM     0         -     67139  14195.28  91814.15  |__sysbench
10:01:54 AM     0         -     67140  19863.21  84233.96  |__sysbench
10:01:54 AM     0         -     67141  17538.68  81353.77  |__sysbench
10:01:54 AM     0         -     67142  14560.38  80265.09  |__sysbench
10:01:54 AM     0         -     67143  19767.92  72928.30  |__sysbench

从上面的数据来看,虽然 sysbench进程的上下文切换次数不多,但是,它的子线程切换却有很多,所以,上下文切换的罪魁祸首,就是 sysbench 线程了。

还有一个问题也别忘记,我们用 vmstat 看的时候,发现中断次数也上升到了 2w 多,那中断的信息要怎么查看呢,答案是从/proc/interrupts文件中读取:

# -d 参数表示高亮显示变化的区域
watch -d cat /proc/interrupts
           CPU0       CPU1
...
LOC:    8239839    9176016   Local timer interrupts
...
RES:   17266140   17259862   Rescheduling interrupts
...

观察一段时间,你可以发现,变化速度最快的是重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。其次为 时间中断(LOC),也是因为进程时间片用完,被系统强制中断。

综合以上工具,我们可以系统的分析上下文切换和 cpu 负载的关系,比如:

  • 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;
  • 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;
  • 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容