深入理解内核态和用户态
1. 内核态和用户态、内核线程和用户线程等解释
操作系统调度CPU的最小单元是线程,也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
系统的用户空间和内核空间:
虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。当进程运行在内核空间时就处于**内核态**,当进程运行在用户空间时就处于**用户态**,为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。说起这个概念就是因为线程上下文切换的概念。虽然线程上下文切换比进程切换成本要低但是,线程切换也是很影响性能的。线程上下文切换就涉及用户态到内核态的转换。
线程的实现可以分为两类:
- 1、用户级线程(User-Level Thread)
- 2、内核线线程(Kernel-Level Thread);而java线程就是内核级线程。
1.1 用户线程(User-Level Thread)
不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
优点:
- 线程的调度不需要内核直接参与,控制简单。
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
- 允许每个进程定制自己的调度算法,线程管理比较灵活。
- 线程能够利用的表空间和堆栈空间比内核级线程多。
- 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
缺点:
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用。
1.2 内核线程(Kernel-Level Thread)
由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。
优点:当有多个处理机时候,一个进程的多线程可以同时执行。
缺点:由内核进行调度。
混合线程
混合线程是内核级别线程和用户级别线程的混合体,提供了以上两种模型的优点,但是带来很大的复杂性。
1.3 ULT和KLT之间的关系
ULT和KLT区别:
程序执⾏的时候实际上分为两种状态,这个状态会被⼀条线划分,上⾯称之为⽤户态,下⾯称之为系统/内核态。⽤户态执⾏的都是我们⾃⼰写的代码,⽐如我们做的登录、⽤户CPU时间⽚分配⽅式。但是这些都是由操作系统做⽀持的,操作系统⽀持的时候就得进⼊系统态。举个例⼦调⽤⽂件读写操作,实际上是调⽤类似 open 的 API,这个API最终是由操作系统实现的,操作系统实际上会把API翻译成具体的系统调⽤ syscall,然后在操作系统⾥⾯执⾏⼀些代码,所以说这个代码实际上分为⽤户态代码和系统
态代码。当从⽤户态代码进⼊系统态代码调⽤的时候会涉及到上下⽂切换,这是要付出⼀定的代价的。很显然系统线程去创建去调度是要付出这些代价的,所以很多时候系统线程成本会⾮常的⾼,当我们频繁的去创建系统线程销掉系统线程这种代价实在太⼤了。
1.4 java线程
java线程生命周期:
休眠状态(BLOCKED、WAITING、TIMED_WAITING)与RUNNING状态的转换
1、RUNNING状态与BLOCKED状态的转换
线程等待 synchronized 的隐式锁,RUNNING —> BLOCKED
线程获得 synchronized 的隐式锁,BLOCKED —> RUNNING
2、RUNNING状态与WAITING状态的转换
获得 synchronized 隐式锁的线程,调用无参数的Object.wait()方法
调用无参数Thread.join()方法
调用LockSupport.park()方法,线程阻塞切换到WAITING状态,
调用LockSupport.unpark()方法,可唤醒线程,从WAITING状态切换到RUNNING状态
3、RUNNING状态与TIMED_WAITING状态的转换
调用带超时参数的 Thread..sleep(long millis)方法
获得 synchronized 隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法
调用带超时参数的Thread.join(long millis)方法
调用带超时参数的LockSupport.parkNanos(Object blocker,long deadline)方法
调用带超时参数的LockSupport.parkUntil(long deadline)方法
1.5 线程上下文切换
线程上下文切换过程:
程序计数器的作用:
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。且由于java虚拟机的多线程是通过线程轮流切换并分配器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器是一个内核)都只会执行一条线程中的指令,因为为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,我们称这类区域为“线程私有”的内存。
线程切换时需要知道在这之前当前线程已经执行到哪条指令了,所以需要记录程序计数器的值,另外比如说线程正在进行某个计算的时候被挂起了,那么下次继续执行的时候需要知道之前挂起时变量的值时多少,因此需要记录CPU寄存器的状态。所以一般来说,线程上下文切换过程中会记录程序计数器、CPU寄存器状态等数据。
1.6 CPU状态之间的转换:
用户态--->内核态:唯一途径是通过中断、异常、陷入机制(访管指令)
内核态--->用户态:设置程序状态字PSW
以下三种情况会导致用户态到内核态的切换:
- 1)系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。比如前例中fork()实际上就是执行了一个创建新进程的系统调用。
而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
- 2)异常
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
- 3)外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,
如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
2. cpu上下文切换
Linux 是个多用户系统,支持大于cpu核数的任务在系统上运行所以不可避免的出现cpu资源竞争,竞争CPU会导致 上下文切换
CPU上下文切换:
CPU在不同的任务之前切换需要保存任务的运行资源记录:CPU得知道从哪里去加载任务,又从哪里开始运行所以需要用到CPU寄存器和程序计数器。在理解上面的基础上CPU上下文切换就是保存上一个任务运行的寄存器和计数器信息切换到加载下一个任务的寄存器和计数器的过程
cpu上下文切换分类:
- 进程上下文切换
- 线程上下文切换
- 中断上下文切换