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性能优化实战)的个人总结