目录
前言
很多像Kafka这种在磁盘上存取文件的系统来说,底层都喜欢用个0拷贝,概念都懂,其实就是避免了用户态和内核态的上下文切换。并不是真正意义上的0拷贝。上一篇写了平均负载,有个疑问,为什么出于准备运行和等待I/O的进程会增大系统负载呢?他们都还没用到CPU吧?其实负载之所以高,就是因为上下文切换导致的。
上下文切换
上面图中的kernel context 和 User context就是所谓的上下文了。
1.什么是上下文
对于上下文这个东西一直很困惑,它到底是个什么,我们经常遇到这个概念不是吗?尤其在Spring框架里,我们又叫容器又叫Bean的上下文。顾名思义吧,一品文章,你只取中间一个段落去看的话,肯定看不懂这篇文章在讲什么,你只有把这段的“上文”和“下文”都补齐了,勾画出一个完整的予以空间,完整的背景,你猜能读懂这一段的含义。也就是说如果没有“上下文”单独把这一段拎出来看是没有意义的,你不知道拿它做什么。也就是说上下文其实就是一个环境。
众所周知,一个系统是可以允许数量远远高于CPU数量的进程同时运行的,当然这个“同时运行”得打双引号。因为事实上并不是同时运行,一个进程运行一个CPU时间片后就会让出时间片,让另一个进程继续运行。我们感知上的同事运行,仅仅只不过是因为CPU时间片小的让我们无法察觉罢了。就像使用交流电的电脑屏幕灯泡其实都是以一定的频率闪烁的,只不过这个频率人类无法用眼睛感知到罢了(题外话)
既然有进程让出时间片,那它下次继续运行的时候CPU和程序怎么知道从哪个地方开始呢?当然是提前要将程序运行的位置存放CPU寄存器和程序计数器,CPU执行命令就指望他们了,CPU寄存器,顾名思义是一块存取速度很快的存储单元,用来存放CPU指令存放地址。程序计数器,其实就是存放程序运行的行号。这两个单元共同构成了CPU的上下文是CPU执行命令强依赖的环境。
2. CPU上下文切换
理解了CPU上下文没那么CPU上下文切换也就不难理解了。
就是进程A让出时间片的时候,要将计算结果和指令地址写入CPU寄存器,将程序运行位置参数写入程序计数器里面。再把新线程的这些信息从CPU寄存器和程序计数器中加载出来。而CPU片段切换是由操作系统任务调度来完成的。根据场景的不同这里的任务可以分为以下三种:
进程上下文切换
Linux根据CPU特权等价将进程运行的空间分为:用户空间和讷河空间。
如图所示,内核空间和用户空间分别对应图中的Ring0和Ring3
- 内核空间:享有最高CPU权限,可以直接访问任何数据
- 用户空间:享有部分受限资源的访问权限,不能直接读取硬件设备上的数据。如需访问,要通过操作系统任务,深入内核空间去访问。
也就是说进程既可以在用户空间运行也可以在内核空间运行。在用户空间运行叫做用户态,在内核空间运行叫做内核态。从用户态到内核态需要操作系统调度任务来完成。比如,要读一个文件操作系统就需要调用open()打开文件,调用read()读取文件到readbuffer再调用writ()写入标准输出最后调用close()关闭文件。
用户态和内核态上下文切换:
- CPU寄存器中用户态的指令地址要先保存起来
- CPU寄存器中读取内核态中运行程序的指令地址
恢复到用户态上下文切换:
- CPU寄存器中内核态的指令地址要先保存起来
- CPU寄存器中读取用户态中运行程序的指令地址
也就是说系统调用发生了两次上下文切换。但是这并不是进程上线文切换,因为是同一线程的上下文切换而不是一个进程,切换到另一进程。事实上这是特权模式切换,但不可避免的有上下文切换的存在。那么进程上线文切换又是什么样的呢?
进程之间的切换是内核来管理的,因此线程的切换是发生在内核态的。通过JVM还知道进程的切换CPU寄存器不仅要保存虚拟内存,栈的信息还需要保存内核态还需要保存内核空间的信息(堆,栈)。而且,加载了下一进程后还需要刷新虚拟堆栈信息。
上下文切换并不是免费的,每次上下文切换都需要花费几微秒的时间,几十纳秒,几微妙也许不多,但如果像Kafka这种高吞吐量的系统呢?极少成多。这是我们接受不了的。
关于零拷贝技术我们知道,Linux是通过TLB(Translation Lookaside Buffer)来管理虚拟内存和真实内存之间的映射关系的。但是虚拟内存改变是必要的刷新TLB,内存访问就会因此变慢。内存读取便面不光是影响当前CPU的性能还影响共享内存的CPU的性能。总之进程上下文切换的场景有以下几种:
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。最后一个,发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。了解这几个场景是非常有必要的,因为一旦出现上下文切换的性能问题,它们就是幕后凶手。
总之上下文切换就不是什么好事,在研发过程中尽量要避免上下文切换的事情发生。保证性能!
线程上下文切换
进程上下文切换,我们说是操作系统的的任务调度,发生在内核空间。事实上,这个任务是线程,而不是进程。因为进程是资源拥有的基本单位,而线程才是任务调度的基本单位。
进程是线程的集合,那么线程的上下文切换就可以分为两种情况:
1.同进程的线程上下文切换
由于是同一进程,共享的想虚拟堆内存就不用做修改,保持原样。只切换线程私有的像虚拟栈,CPU寄存器不共享的数据。
2.不同进程的线程上下文切换
由于不同进程,那就是进程切换。跟之前说的进程上下文切换一样。
中断上下文切换
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
特点就是跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当你发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
上下文切换查看
# 每隔5秒输出1组数据
$ 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 7005360 91564 818900 0 0 0 0 25 33 0 0 100 0 0
- cs(context switch)是每秒上下文切换的次数。
- in(interrupt)则是每秒中断的次数。
- r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
- b(Blocked)则是处于不可中断睡眠状态的进程数。
每个进程的上下文切换情况
用前面提到过的pidstat加 -w
# 每隔5秒输出1组数据
$ pidstat -w 5
Linux 4.15.0 (ubuntu) 09/23/18 _x86_64_ (2 CPU)
08:18:26 UID PID cswch/s nvcswch/s Command
08:18:31 0 1 0.20 0.00 systemd
08:18:31 0 8 5.40 0.00 rcu_sched
...
- cswch (voluntary context switches):表示每秒自愿上下文切换的次数
- nvcswch(non voluntary context switches):表示每秒非自愿上下文切换的次数。
- 自愿上下文切换:程序无法获取资源导致的上下文切换,比如I/O,比如内存资源不够时就会发生上下文切换。
- 非自愿上下文切换:进程让出CPU时间段发生的上下文切换
上下文切换频率是多少次才算正常
运行 vmstat,观察其中参数:
# 间隔1秒后输出1组数据
$ 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 6984064 92668 830896 0 0 2 19 19 35 1 0 99 0 0
# 每隔1秒输出1组数据(需要Ctrl+C才结束)
$ 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
6 0 0 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0
- cs列:参数上下文切换次数从之前的 35 骤然上升到了 139 万
- r 列:就绪队列的长度已经到了 8,远远超过了系统 CPU 的个数 2,所以肯定会有大量的 CPU 竞争。
- us(user)和 sy(system)列:这两列的 CPU 使用率加起来上升到了 100%,其中系统 CPU 使用率,也就是 sy 列高达 84%,说明 CPU 主要是被内核占用了。
- in 列:中断次数也上升到了 1 万左右,说明中断处理也是个潜在的问题。
分析结论:系统的就绪队列过长,也就是正在运行和等待 CPU 的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 CPU 的占用率升高
# 每隔1秒输出1组数据(需要 Ctrl+C 才结束)
# -w参数表示输出进程切换指标,而-u参数则表示输出CPU使用指标
$ pidstat -w -u 1
08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2
08:06:33 UID PID cswch/s nvcswch/s Command
08:06:34 0 8 11.00 0.00 rcu_sched
08:06:34 0 16 1.00 0.00 ksoftirqd/1
08:06:34 0 471 1.00 0.00 hv_balloon
08:06:34 0 1230 1.00 0.00 iscsid
08:06:34 0 4089 1.00 0.00 kworker/1:5
08:06:34 0 4333 1.00 0.00 kworker/0:3
08:06:34 0 10499 1.00 224.00 pidstat
08:06:34 0 26326 236.00 0.00 kworker/u4:2
08:06:34 1000 26784 223.00 0.00 sshd
# 每隔1秒输出一组数据(需要 Ctrl+C 才结束)
# -wt 参数表示输出线程的上下文切换指标
$ pidstat -wt 1
08:14:05 UID TGID TID cswch/s nvcswch/s Command
...
08:14:05 0 10551 - 6.00 0.00 sysbench
08:14:05 0 - 10551 6.00 0.00 |__sysbench
08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
08:14:05 0 - 10554 18827.00 103954.00 |__sysbench
...
碰到上下文切换次数过多的问题时,我们可以借助 vmstat 、 pidstat 和 /proc/interrupts 等工具,来辅助排查性能问题的根源