CPU上下文的切换
在理解了系统内核的调用之后,我们再来理解多个进程之间的切换:
多个进程之间的切换
通常进程的切换是需要由内核态进行控制的,所以一般进程切换过程中难免会需要进行系统的内核调用(也就是上边我所讲到的点)。
在内核态中,会有统一的机制进行这些进程的调度管理,这里面需要:
对cpu的寄存器值进行更新。
保存和更新一些进程的虚拟内存信息,栈信息等。
线程之间的切换
同一个进程之间的线程切换
同一个进程里虚拟内存是共享的,所以只需要更新寄存器相关的地址,在进行上下文切换的时候开销会较小一些,但是依旧难免需要进行系统的调用。
不同进程之间的线程切换
由于虚拟内存不同,所以在进行上下文切换的时候和多进程之间切换差不多。
中断上下文的切换
中断上下文切换的时候,主要发生在用户内核态,中断处理会打断进程的正常的调度和执行,被打断的进程或者线程需要暂时记录下它的上下文信息。过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。
进程上下文切换跟系统调用又有什么区别?
首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。
如何分析操作系统中的cpu上下文切换情况?
vmstat指令:每隔三秒查看一次操作系统整体的上下文切换情况
vmstat 3
案例:
测试机器查看情况
cs(context switch)是每秒上下文切换的次数。 in(interrupt)则是每秒中断的次数。 r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。 b(Blocked)则是处于不可中断睡眠状态的进程数。 复制代码
pidstat指令:查看整体的切换次数可能在实际问题排查的时候会感觉还不够用,所以这个时候可以结合pidstat指令查看每个线程的上下文切换详情。
pidstat -w 复制代码
自愿上下文切换(cswch)
是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
非自愿上下文切换(nvcswch)
则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。
自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题。
非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈。
如何理解进程,线程,纤程
进程
分配资源的单位 (分配虚拟内存空间)
线程
执行程序的调度单位(不会分配任何内存空间,共享进程的内存空间)
纤程(fiber)
JVM 运行在用户空间,当它 new 一个 Thread 时,会对应在 OS 中起一个线程(内核空间,通常消耗内存大小为1m左右),所以这叫重量级线程。而纤程则是在用户态自己起一个逻辑空间,在语言层面进行调度, 和线程比起来,协程的切换不需要操作系统进行保存和恢复CPU 上下文,自己的缓存数据等,因为所有的协程都存在于同一个线程之中,所以协程的切换开销很小。一个线程包含多个纤程,又叫协程,只是协程是一个概念,而纤程是 Windows 系统对协程的一个具体实现。
Fiber在语言层面中通过用户态自己维护的栈空间来管理程序计数器,寄存信息保存等操作,不需要和os内核态打交道,这一点比线程要高效许多。
所谓市面上常说的绿色线程更多的是一个逻辑层面的概念,依赖于虚拟机来实现。操作系统对于虚拟机内部如何进行线程的切换并不清楚,从虚拟机外部来看,或者说站在操作系统的角度看,这些都是不可见的。
了解了纤程之后,我们再来看看go语言内部的设计。goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,几十个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。
目前也有一些人私底下开发了些类库以支持Java语言使用fiber,但是不是特别成熟,截止到JDK13,Java对fiber的支持不是很友好,据说JDK14会较好的支持,蛮期待的。