1、什么是上下文
首先,需要讲清楚什么是上下文。
每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,这就涉及到 CPU 寄存器 和 程序计数器(PC):
【1】CPU 寄存器是 CPU 内置的容量小、但速度极快的内存;
【2】程序计数器会存储 CPU 正在执行的指令位置,或者即将执行的指令位置。
这两个是 CPU 运行任何任务前都必须依赖的环境,因此叫做 CPU 上下文。
2、什么是上下文切换
上下文切换就是把前一个任务的CPU上下文保存起来,然后加载新任务的上下文到这些指令寄存器(IR)和程序寄存器(PC)等寄存器中。这些被保存下来的上下文会存储在操作系统的内核中,等待任务重新调度执行时再次加载进来,这样就能保证任务的原来状态不受影响,让任务看起来是连续运行的。
这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
3、什么时候会发生上下文切换?
按导致上下文切换的因素划分,可将上下文切换分为两点:
(1)自发性上下文切换
自发性上下文切换指线程由于自身因素导致的切出。通过调用下列方法会导致自发性上下文切换:
Thread.sleep()
Object.wait()
Thread.yeild()
Thread.join()
LockSupport.park()
(2)非自发性上下文切换
非自发性上下文切换指线程由于线程调度器的原因被迫切出。发生下列情况可能导致非自发性上下文切换:
【1】切出线程的时间片用完
【2】有一个比切出线程优先级更高的线程需要被运行
【3】虚拟机的垃圾回收动作
4、上下文切换的开销
上下文切换的开销包括直接开销和间接开销。
直接开销有如下几点:
【1】操作系统保存回复上下文所需的开销
【2】线程调度器调度线程的开销
间接开销有如下几点:
【1】处理器高速缓存重新加载的开销
【2】上下文切换可能导致整个一级高速缓存中的内容被冲刷,即被写入到下一级高速缓存或主存
5、查看系统的上下文切换情况
既然过多的上下文切换会把CPU的时间消耗在上下文环境的保存上,并没有充分利用其计算功能。那就需要查看当前系统的上下文切换情况了。
vmstat
查看系统的上下文切换情况
$ 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
1 0 0 2453028 2108 1211540 0 0 3 10 27 35 0 0 99 0 0
0 0 0 2453004 2108 1211572 0 0 0 0 52 66 0 0 100 0 0
- cs (centext switch) 每秒的上下文切换次数
- in (interrupt) 每秒的中断次数
- r (Runing or Runnable) 就绪队列的长度,也就是正在运行和等待CPU的进程数。
- b (Blocked) 不可中断睡眠状态的进程数
pidstat
pidstat 可以看到具体的某个应用程序的上下文切换情况。
$ pidstat -w 1
Linux 3.10.0-957.el7.x86_64 (localhost.localdomain) 2019年03月23日 _x86_64_ (2 CPU)
13时51分31秒 UID PID cswch/s nvcswch/s Command
13时51分32秒 0 9 9.80 0.00 rcu_sched
13时51分32秒 0 11 0.98 0.00 watchdog/0
13时51分32秒 0 12 0.98 0.00 watchdog/1
13时51分32秒 0 481 0.98 0.00 kworker/1:3
13时51分32秒 0 4683 18.63 0.00 xfsaild/dm-0
13时51分32秒 0 9433 9.80 0.00 vmtoolsd
13时51分32秒 0 27261 0.98 0.00 kworker/u256:0
13时51分32秒 0 40878 2.94 0.00 kworker/0:1
13时51分32秒 0 40880 0.98 0.00 pidstat
- cswch (voluntary context switches) 自愿上下文切换,指的是进程无法获得所需的资源导致的上下文切换。比如I/O不足,内存不足。
- nvcswch (non voluntary context switches) 非自愿上下文切换,指的是 进程由于时间片已到等原因,被系统强制调度,进而发生上下文切换。比如大量进程在争抢CPU
模拟上下文切换的场景
这里使用的sysbench,模拟操作系统的多线程调度瓶颈。以10个线程运行5分钟的基准测试。模拟多线程切换
$ sysbench --threads=100 --max-time=300 threads run
观察vmstat的输出
$ 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
17 0 0 2442576 2108 1217720 0 0 3 10 27 80 0 0 99 0 0
11 0 0 2442576 2108 1217720 0 0 0 0 23882 1325686 10 89 1 0 0
10 0 0 2442576 2108 1217720 0 0 0 0 22815 1348568 11 89 0 0 0
17 0 0 2442576 2108 1217720 0 0 0 0 23978 1328481 10 89 1 0 0
11 0 0 2442576 2108 1217720 0 0 0 25 23081 1344558 11 89 0 0 0
14 0 0 2442576 2108 1217720 0 0 0 0 23804 1305193 11 89 1 0 0
19 0 0 2442576 2108 1217720 0 0 0 0 22418 1254798 11 89 0 0 0
可观察到 cs 瞬间上升(第一行是开机以来的参数的平均值)。
接下来观察pidstat ,这里需要加上 -t 参数,显示线程,要不不带参数,看不到sysbench。
$ pidstat -u -w 1
平均时间: UID PID %usr %system %guest %CPU CPU Command
平均时间: 0 42213 0.00 0.05 0.00 0.05 - kworker/0:0
平均时间: 0 42218 19.82 100.00 0.00 100.00 - sysbench
平均时间: 0 42321 0.33 1.00 0.00 1.33 - pidstat
平均时间: UID PID cswch/s nvcswch/s Command
平均时间: 0 2 0.05 0.00 kthreadd
平均时间: 0 3 0.29 0.00 ksoftirqd/0
平均时间: 0 9 8.32 0.00 rcu_sched
平均时间: 0 11 0.24 0.00 watchdog/0
平均时间: 0 12 0.24 0.00 watchdog/1
平均时间: 0 14 0.90 0.00 ksoftirqd/1
平均时间: 0 37 0.10 0.00 khugepaged
加上-t后
可以看到 sysbench 发生了大量的自愿上下文切换
$ pidstat -wut 1
平均时间: 0 27261 - 1.85 0.00 kworker/u256:0
平均时间: 0 - 27261 1.85 0.00 |__kworker/u256:0
平均时间: 997 - 27300 1.85 0.00 |__grafana-server
平均时间: 997 - 27304 0.93 0.00 |__grafana-server
平均时间: 997 - 27307 1.85 0.00 |__grafana-server
平均时间: 997 - 27341 1.85 0.00 |__grafana-server
平均时间: 0 - 42219 2879.63 11057.41 |__sysbench
平均时间: 0 - 42220 1902.78 13955.56 |__sysbench
平均时间: 0 - 42221 4470.37 11027.78 |__sysbench
平均时间: 0 - 42222 1792.59 17249.07 |__sysbench
平均时间: 0 - 42223 2542.59 7395.37 |__sysbench
平均时间: 0 - 42224 1183.33 16775.93 |__sysbench
平均时间: 0 - 42225 3678.70 12963.89 |__sysbench
平均时间: 0 - 42226 3208.33 15801.85 |__sysbench
平均时间: 0 - 42227 2602.78 13896.30 |__sysbench
通过dstat命令可以看到,除了有很多的资源上下文切换,还有很多中断。
$ dstat
You did not select any stats, using -cdngy by default.
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
1 2 97 0 0 0|5683B 19k| 0 0 | 0 0 | 583 30k
8 85 7 0 0 0| 0 0 | 132B 1060B| 0 0 | 28k 977k
8 82 10 0 0 0| 0 0 | 132B 620B| 0 0 | 25k 1025k
这是重调度中断(RES),在唤醒空闲状态的CPU来重新调度新任务运行。具体可看 /proc/interrupts。
小结
一般上下文切换在数百到一万之内上下文切换超过1万,很可能遇到性能问题。需要具体看看了。
- 资源上下文切换时说明进程在等待资源,有可能发生了I/O等问题;
- 非自愿上下文切换,说明进程在被强制调度,也就是在争抢CPU;
- 中断次数多了,说明CPU在被中断处理程序占用。可以通过/proc/interrupts 查看。
参考:
《Java并发编程的艺术》
《Java多线程编程实战指南》
《Linux性能优化实战》
https://www.jianshu.com/p/1c80cd47e8cf