写给Java程序员看的,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状态–用户态
。
系统调用会进行 内核态
、用户态
转换