上下文切换的那些事儿

1. CPU上下文

前面说过,多个进程竞争CPU会导致平均负载升高,可是这个时候进程并没有真正运行,真正作怪的其实是CPU上下文切换。Linux是一个多任务系统,支持大于CPU数量的任务同时运行,这里的同时运行并不是真正意义上的“同时”,而是由系统在很短时间内将CPU轮流分配给任务造成的错觉。

在每个任务运行前,CPU都要知道从哪里加载,又从哪里开始运行,这就需要事先帮它设置好CPU寄存器和程序计数器。CPU寄存器是内置在CPU中的容量小但速度极快的内存,而程序计数器则是用来存储CPU正在执行的指令位置,或者即将执行的下一条指令的位置。它们都是CPU在执行任务前,必须依赖的环境,叫做CPU上下文

那么,CPU上下文切换,就是把前一个任务的CPU上下文保存起来,再加载进去新任务的CPU上下文,最后再跳转到程序计数器所指的新位置,执行新任务,保存起来的CPU上下文会保存在系统内核中,并在任务重新调度执行时再次加载进来,这样就保证原来任务的状态不受影响,让任务看起来是在连续运行。可是,这些寄存器本身就是为了快速运行任务而设计的,为什么会影响系统的CPU性能?后面会逐步解释。

我们再来看看这些任务是什么:进程、线程、硬件触发信号而导致中断处理程序的执行。所以根据任务的不同,CPU的上下文切换可以分为三个场景:进程上下文切换、线程上下文切换、中断上下文切换。

2. 进程上下文切换

                                              

Linux按照特权等级,把进程的运行空间分为内核空间和用户空间,对应着上图CPU特权等级的 Ring0 和 Ring3 。Ring0为最高权限,可以访问所有等级的资源;Ring3为最低权限,只能访问用户空间的资源。如果用户空间想访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问特权资源。Ring1和Ring2等级在Linux中还没用到。

进程在用户空间运行,称为进程的用户态;陷入内核空间运行,称为进程的内核态。从用户态到内核态的转变需要系统调用,比如读取文件内容,需要进行多次系统调用来完成:open、read、write到标准输出、close。系统调用实际上发生了CPU上下文切换的。CPU寄存器里原来用户态指令的位置需要先保存起来,然后更新为内核态指令的位置,最后才跳转到内核态运行内核任务。当系统调用结束,CPU寄存器需要恢复成原来保存的用户态,再跳转到用户空间,继续运行进程。因此,一次系统调用发生了两次CPU上下文切换。

但是,系统调用的过程,不会涉及虚拟内存等用户态的资源,也不会切换进程,所以系统调用通常称为特权模式切换而不是上下文切换,但会涉及到CPU上下文切换。说到现在才开始讲进程上下文切换,进程是由内核来管理和调度的,进程的切换只能的发生在内核态。所以进程上下文切换不仅包括虚拟内存、栈、全局变量等用户空间资源,还包括内核堆栈、寄存器等内核空间资源。因此,进程的上下文切换比系统调用多了一步,还要先把用户空间的资源保存起来,而加载了下一进程的内核态资源后,还要更新进程的虚拟内存和栈。所以,保存上下文和恢复上下文并不是免费的,而是要在CPU上运行。

根据Tsuna测试报告,,每次上下文切换都需要几十纳秒到几微秒的CPU时间,如果上下文切换次数较多,就会更多的将CPU时间的耗费在内核态和用户态资源的保存和恢复上,减少了进程真正运行的时间。另外,Linux通过TLB管理虚拟内存到物理内存的映射关系。所以,当虚拟内存更新,TLB也要更新,内存访问随之变慢,特别多处理器的系统中,缓存是要被多处理器共享的,刷新缓存不仅要影响当前处理的进程,还要影响共享缓存的其他处理器的进程。

Linux为每个CPU维护一个就绪队列,将活跃进程(正在运行或者等待CPU的进程)按照优先级或者等待CPU时间排序,然后选择最需要的进程来运行。那么进程都在哪些场景中会被调度到CPU上运行?

场景一,为了保证所有进程有公平机会得到CPU调度,CPU时间会被划分成时间片,这些时间片轮流分配给各个进程。当某个进程的时间片运行结束,会被系统挂起,切换到其它正在等待CPU的进程。

场景二,进程在系统资源不足的时候,要等到资源满足才会运行,这个时候也会被挂起,由系统调度其他进程运行。

场景三,当进程通过睡眠函数sleep,主动挂起自然也会重新调度。

场景四,当有高优先级的进程需要运行,当前进程会被挂起。

场景五,发生硬件中断,CPU上的进程会被中断挂起,转而执行内核中的中断服务程序。

了解这些场景非常必要,因为一旦出现性能问题,他们就是元凶。

3. 线程上下文切换

线程与进程的区别,线程是调度的基本单位,进程是资源拥有的基本单位。说白了,内核中的任务调度,调度对象就是线程,进程则是给线程提供虚拟内存、全局变量等资源。我们又可以这样理解两者:

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

这么一来线程上下文切换就分为两种情况:

两个线程分属不同进程,因为资源不共享,所以切换过程跟进程上下文切换一样;

两个线程同属一个进程,因为虚拟内存是共享的,所以切换时,虚拟内存这些资源就保持不变,只需切换线程私有数据、寄存器等不共享的数据。

到这里已经发现,进程内的线程切换要比进程间的切换要耗费更少的资源。

4.中断上下文切换

为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要保存进程当前的状态,这样在中断结束后,就可以从原来的状态的恢复运行。跟进程上下文切换相比,中断上下文切换并不涉及进程的用户态,所以即便中断打断了一个正处于用户态的进程,也不需要保存和恢复这个进程的用户态资源。中断上下文,其实只包括内核态中断服务程序执行所需要的资源,包括CPU寄存器、内核堆栈、硬件中断参数等。

对同一个CPU来说,中断处理优先级比进程更高,所以中断上下文切换不会与进程上下文同时发生,而且中断处理程序都应短小精悍,以便于更快执行结束。另外由于中断上下文切换也消耗CPU,切换次数过多也会耗费大量CPU,甚至严重降低系统整体性能。当你发现中断次数过多,就要排查它是否给你系统带来严重性能问题。

5. 怎样查看系统的上下文切换

vmstat是一个常用的系统性能分析工具,主要用来分析系统的内存使用情况、CPU上下文切换和中断次数。

[root@test-server ~]# vmstat 5
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 7381804   2108 388040    0    0     0     0    1    2  0  0 100  0  0
 0  0      0 7381788   2108 388040    0    0     0     0  128  118  0  0 100  0  0

这里特别关注一下四列内容:

cs(context switch):每秒上下文切换次数;

in(interrupt):每秒中断次数;

r(running or runnable):就绪队列长度,也就是正在运行或者等待CPU的进程数;

b(blocked):处于不可中断睡眠状态的进程数。

vmstat只给出系统总的上下文切换情况,要想查看每个进程的详细情况,就要适用前面提到的pidstat工具。

[root@test-server ~]# pidstat -w 5
Linux 3.10.0-957.el7.x86_64 (test-server) 	12/17/2019 	_x86_64_	(4 CPU)

11:45:19 AM   UID       PID   cswch/s nvcswch/s  Command
11:45:24 AM     0         9      1.80      0.00  rcu_sched
11:45:24 AM     0        11      0.20      0.00  watchdog/0
11:45:24 AM     0        12      0.20      0.00  watchdog/1
11:45:24 AM     0        14      0.20      0.00  ksoftirqd/1

cswch/s (voluntary context switch):每秒自愿上下文切换的次数

nvcswch/s (non-voluntary context switch):每秒非上下文切换的次数

自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换,比如 I/O、内存等资源不足时,就会发生上下文切换。

非自愿上下文切换,是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如大量进程在争抢CPU时就会出现。

6. 案例分析

sysbench是一款开源的多线程性能测试工具,可以执行CPU/内存/线程/IO/数据库等方面的性能测试。这里只把它当做异常进程来看,作用是模拟上下文切换过多的情况。

测试系统:centos 7  CPU 4核

执行命令:

以10个线程运行5分钟的基准测试,模拟多线程切换问题
sysbench --threads=16 --max-time=300 threads run

执行结果:

[root@test-server ~]# 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
12  0      0 7367052   2108 399072    0    0     0     0    2    1  0  0 100  0  0
10  0      0 7366780   2108 399092    0    0     0     0 44922 981144 31 68  1  0  0
10  0      0 7366780   2108 399092    0    0     0     0 42793 973179 32 68  1  0  0
 9  0      0 7366780   2108 399092    0    0     0     0 42431 1000661 32 68  1  0  0
12  0      0 7366780   2108 399092    0    0     0     0 37168 994350 31 68  0  0  0

可以发现cs列已经从每秒1次增长到接近100万次,再看其他指标:

r列:就绪队列的长度最多的时候达到12,超过了系统CPU个数4,肯定会有大量CPU竞争。

us 和 sy 列: 两列CPU使用率相加几乎等于100%,其中sy列CPU使用率也就是系统CPU使用率高达68%,说明CPU注意被内核占用。

in 列:中断次数也上升到4万次左右,说明中断处理是个潜在问题。

综合这几个指标,我们大概知道,就绪队列过长,使得正在运行或者等待CPU的进程数过多,导致大量上下文切换,进而使得系统CPU使用率升高。

接下来我们看一下,CPU和进程上下文切换的情况:

[root@test-server ~]# pidstat -w -u  5
Linux 3.10.0-957.el7.x86_64 (test-server) 	12/17/2019 	_x86_64_	(4 CPU)

02:49:33 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
02:49:38 PM     0      6812    0.00    0.79    0.00    0.40    0.79     0  kworker/0:2
02:49:38 PM     0     13618  127.58  264.09    0.00    0.00  391.67     2  sysbench
02:49:38 PM     0     13638    0.00    0.40    0.00    0.20    0.40     0  pidstat

02:49:33 PM   UID       PID   cswch/s nvcswch/s  Command
02:49:38 PM     0         3      4.56      0.00  ksoftirqd/0
02:49:38 PM     0         7      0.20      0.00  migration/0
02:49:38 PM     0         9      7.14      0.00  rcu_sched
02:49:38 PM     0        11      0.20      0.00  watchdog/0
02:49:38 PM     0        12      0.20      0.00  watchdog/1
02:49:38 PM     0        14      3.77      0.00  ksoftirqd/1
02:49:38 PM     0        17      0.20      0.00  watchdog/2
02:49:38 PM     0        19      4.96      0.00  ksoftirqd/2
02:49:38 PM     0        22      0.20      0.00  watchdog/3
02:49:38 PM     0        24      3.57      0.00  ksoftirqd/3
02:49:38 PM     0        62      0.60      0.00  kworker/3:1
02:49:38 PM     0      6812     50.60      1.39  kworker/0:2
02:49:38 PM     0      6846      0.40      0.00  kworker/0:3
02:49:38 PM     0     13418      1.19      0.00  kworker/1:2
02:49:38 PM     0     13617      0.20      0.00  kworker/0:1
02:49:38 PM     0     13638      0.20     68.25  pidstat

可以看到,系统CPU使用率升高果然是sysbench导致,CPU使用率高达391.67%,但上下文切换次数总共加起来也就100多,比上面vmstat显示的100万次差太多,原因出自哪里呢,是不是工具本身的问题呢?其实,pidstat默认展示的是进程的指标数据,加上-t选项,才会展示线程指标数据。

[root@test-server ~]# pidstat -wt  5
Linux 3.10.0-957.el7.x86_64 (test-server) 	12/17/2019 	_x86_64_	(4 CPU)

03:28:20 PM   UID      TGID       TID   cswch/s nvcswch/s  Command
...
03:28:21 PM     0     13421         -    193.00     72.00  sshd
03:28:21 PM     0         -     13421    193.00     72.00  |__sshd
03:28:21 PM     0         -     13672  20179.00  40613.00  |__sysbench
03:28:21 PM     0         -     13673  17076.00  46165.00  |__sysbench
03:28:21 PM     0         -     13674  19567.00  42361.00  |__sysbench
03:28:21 PM     0         -     13675  18764.00  34240.00  |__sysbench
03:28:21 PM     0         -     13676  16127.00  34340.00  |__sysbench
03:28:21 PM     0         -     13677  16898.00  39094.00  |__sysbench
03:28:21 PM     0         -     13678  17950.00  34225.00  |__sysbench
03:28:21 PM     0         -     13679  19122.00  42368.00  |__sysbench
03:28:21 PM     0         -     13680  18320.00  32984.00  |__sysbench
03:28:21 PM     0         -     13681  19004.00  34393.00  |__sysbench
03:28:21 PM     0         -     13682  18408.00  40171.00  |__sysbench
03:28:21 PM     0         -     13683  16418.00  34439.00  |__sysbench
03:28:21 PM     0         -     13684  15711.00  44909.00  |__sysbench
03:28:21 PM     0         -     13685  19345.00  34201.00  |__sysbench
03:28:21 PM     0         -     13686  15070.00  40894.00  |__sysbench
03:28:21 PM     0         -     13687  16993.00  45058.00  |__sysbench
...

结果一目了然,就是过多的sysbench线程导致。

当然除了上下文切换频率骤然升高,还有中断次数也升高了很多,但具体是什么类型的中断呢?我们知道,中断肯定发生在内核,而pidstat只是一个进程性能分析工具,并不能详细查看中断信息。我们可以在文件/proc/interrupts中读取,运行下面命令查看:watch -d cat /proc/interrupts

观察一段时间发现,变化速度最快的是重调度中断(RES),这个中断类型表示,唤醒空闲状态的CPU来调度新的任务运行。这是多处理系统中,调度器用来分撒任务到不同CPU的机制,也被称为处理器间中断(Inter-Processor Interrupts, IPI)。所以这里,中断升高还是因为过多任务调度导致,跟前面上下文切换次数的分析结果一致

回到最初的问题上来,那么系统每秒上下文切换多少次才算正常?

这个数值取决系统CPU本身性能。在我看来,如果系统上下文切换次数较稳定,从几百到一万以内,都算正常,。但当上下文切换次数超过一万次或者切换次数出现数量级增长,很可能性能已经出现问题。这是,你还需要根据上下文切换类型,做具体分析。比方说:

自愿上下文切换次数过多,说明进程都在等待资源,有可能发生IO等其他问题;

非自愿上下文切换次数增多,说明进程都在被强制调度,也就是都在争强CPU,CPU出现瓶颈;

中断次数变多,说明CPU被中断处理程序占用,需要查看/proc/interrupts文件来获取具体中断类型。

以上是学习极客时间专栏(倪朋飞:Linux性能优化实战)的个人总结

https://time.geekbang.org/column/intro/140

发布了37 篇原创文章 · 获赞 20 · 访问量 4942

猜你喜欢

转载自blog.csdn.net/qq_24436765/article/details/103700487