CPU 上下文切换、用户态、内核态、进程与线程上下文切换
1、概述
JDK源码中很多Native方法,特别是多线程、NIO部分,很多功能需要操作系统功能支持,作为Java程序员,如果要理解和掌握多线程和NIO等原理,就需要对操作系统的原理有所了解。
2、CPU 上下文切换
多任务操作系统中,多于CPU个数的任务同时运行就需要进行任务调度,从而多个任务轮流使用CPU。
从用户角度看好像所有的任务同时在运行,实际上是多个任务你运行一会,我运行一会,任务切换的速度很快,我们感觉不到而已。
而每个任务运行前,CPU需要知道从哪里加载这个任务的程序,还需要知道从程序哪行开始执行,这就要求OS事先帮任务设置好CPU的 寄存器 和程序计数器 。
CPU执行任务必须依赖的环境称为 CPU上下文
CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
CPU的上下文切换分为几种场景:进程上下文切换、线程上下文切换、中断上下文切换
2.1、用户态、内核态
Linux按特权等级,将进程的运行空间分为 内核空间 和 用户空间 。
Intel x86架构使用了4个级别来标明不同的特权级权限。
R0实际就是内核态,拥有最高权限,可以直接访问所有资源(包括外围设备,例如硬盘,网卡等)。而一般应用程序处于R3状态–用户态。
进程在用户空间运行时,被称为进程的 用户态,而陷入内核空间的时候,被称为进程的 内核态。
R0最高可以读取R0-3所有的内容,R1可以读R1-3的,R2以此类推,R3只能读自己的数据。
2.2、为什么分内核态和用户态
假设没有这种内核态和用户态之分,程序随随便便就能访问硬件资源,比如说分配内存,程序能随意的读写所有的内存空间,如果程序员一不小心将不适当的内容写到了不该写的地方,就很可能导致系统崩溃。
用户程序进行系统调用后,操作系统执行一系列的检查验证,确保这次调用是安全的,再进行相应的资源访问操作。内核态能有效保护硬件资源的安全。
2.3、系统调用
从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
系统调用会将CPU从用户态切换到核心态,以便 CPU 访问受到保护的内核内存。
系统调用的过程会发生 CPU 上下文的切换,CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。
注意:系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。
系统调用过程通常称为特权模式切换,而不是进程上下文切换。
3、进程上下文切换
进程上下文切换跟系统调用又有什么区别呢?
首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。
因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、用户栈等保存下来;而加载下一个进程的内核态后,还需要加载这个进程的虚拟内存和用户栈。
根据 Tsuna 的测试报告,每次上下文切换都需要几十纳秒到数微秒的 CPU 时间,在进程上下文切换次数较多的情况下,这个时间对于CPU来说是相当可观的,会大大缩短CPU真正用于运行进程的时间。
3.1、什么时候会切换进程上下文?
只有在进程调度的时候,才需要切换上下文。
Linux 为每个 CPU 都维护了一个就绪队列,将活跃进程(即正在运行和正在等待 CPU 的进程)按照优先级和等待 CPU 的时间排序,然后选择最需要 CPU 的进程,也就是优先级最高和等待 CPU 时间最长的进程来运行。
新进程在什么时候才会被调度到 CPU 上运行呢?
1. 运行中的进程执行完终止了,CPU 会释放出来,新的基础进程就可以被调度到CPU上运行了。
2. 运行中的进程时间片用完,进程被挂起
3. 运行中的进程资源不足,进程被挂起
4. 运行中的进程执行Sleep方法主动挂起
5. 新进程优先级更高,运行中的进程被挂起
6. 发生硬件中断,运行中的进程会被中断挂起,转而执行内核中的中断服务程序。
4、线程上下文切换
线程是调度的基本单位,而进程则是资源拥有的基本单位。
所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
当进程只有一个线程时,可以认为进程就等于线程,当进程拥有多个线程时,这些线程会共享进程的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
线程的上下文切换其实就可以分为两种情况:
两个线程属于不同进程,因为资源不共享,切换过程和进程上线文切换一样
两个线程属于同一个进程,只需要切换线程的私有数据、寄存器等不共享的数据
5、总结
CPU上线文切换,切换寄存器、程序计数器
进程上线文切换,切换虚拟内存、用户栈
线程上下文切换,2种情况:(1)线程私有数据(比如线程栈、程序计数器等);(2)、(1)+ 线程资源 ;
系统调用:需要进行线程上下文切换,但不是进程上下文切换
R0实际就是内核态,拥有最高权限,可以直接访问所有资源(包括外围设备,例如硬盘,网卡等)。
应用程序处于R3状态–用户态。
系统调用会进行 内核态、用户态 转换