线程并发-内核态和用户态

304 阅读5分钟

概念

在操作系统中,程序的执行权限有两种:用户态和内核态。

  • 内核态
    cpu可以访问计算机所有的软硬件资源
  • 用户态
    cpu权限受限,只能访问到自己内存中的数据,无法访问其他资源

出现原因

程序拥有对所有硬件的操作权限时,可以直接访问任意地址的内存,控制所有硬件资源,这种情况下,单个程序的错误操作将带来灾难性的宕机,影响所有程序和操作系统运行,只能硬件重启恢复。 为了尽量隔离程序和硬件,操作系统需要限制程序的访问操作权限,在必要的时候才能通过操作系统获取高级权限。

设计

通过 CPU 指令集权限级别,程序底层可以使用两种权限级别的 CPU 指令集。

  • 内核态下的程序使用ring 0 级别指令即所有指令
  • 用户态下的程序使用ring 3级别指令,没有对硬件的直接控制权限,也不能直接访问地址的内存,程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存,在这种保护模式下,即使程序发生崩溃也是可以恢复的,在电脑上大部分程序都是在,用户模式下运行的

权限切换

切换条件

  • 系统调用(软中断):用户态进程主动切换到内核态的方式,用户态进程通过系统调用向操作系统申请资源完成工作,例如 fork()就是一个创建新进程的系统调用,系统调用的机制核心使用了操作系统为用户特别开放的一个中断来实现,如Linux 的 int 80h 中断
  • 异常:当 C P U 在执行用户态的进程时,发生了一些没有预知的异常,这时当前运行进程会切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常
  • 硬中断:当 C P U 在执行用户态的进程时,外围设备完成用户请求的操作后,会向 C P U 发出相应的中断信号,这时 C P U 会暂停执行下一条即将要执行的指令,转到与中断信号对应的处理程序去执行,也就是切换到了内核态。如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后边的操作等

切换需要保存的资源

  • 进程栈 每个进程都有两个栈,分别是用户态的栈与内核态的栈。对执行单元的上下文环境进行切换是由栈这个核心数据结构支撑的。因为函数在执行的时候,就会在栈上创建栈帧,那么函数执行的上下文都将保存在栈帧里。
  • 寄存器 寄存器保存着线程的上下文,对应着当前运行的状态,例如说当前函数运行到哪个位置了

切换过程

当发生 用户态 -> 内核态 -> 用户态 的切换时,会发生如下过程:

  1. 设置处理器至内核态
  2. 保存当前寄存器的值,例如堆栈指针寄存器(RSP寄存器)、程序计数器(PC)、通用寄存器
  3. 将栈指针设置指向内核栈地址
  4. 将程序计数器设置为一个事先约定的地址上,该地址上存放的是系统调用处理程序的起始地址
  5. 而之后从内核态返回用户态时,又会进行类似的工作

切换性能问题

用户态和内核态之间的切换有一定的开销,如果频繁发生切换势必会带来很大的开销,所以要尽量减少切换。 如何减少用户态和内核态之间的切换:

  • 减少线程内部运行逻辑引发的切换,把尽量多的任务放在用户态处理
  • 减少线程之间的切换引发的切换
    • 无锁并发编程。多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换
    • CAS算法。使用CAS避免加锁,避免阻塞线程
    • 使用最少的线程。避免创建不需要的线程协程。在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

在编程语言中

JAVA

JAVA线程是混合型的线程模型,一般而言是通过lwp将用户级线程映射到内核线程中

  • 不是纯粹用户级线程
    JAVA中有个fork join框架,这个框架是利用多处理技术进行maprudce的工作,也就证明了内核是可以感知到用户线程的存在,因此才会将多个线程调度到多个处理器中。还有,JAVA应用程序中的某个线程阻塞,是不会引起整个进程的阻塞,从这两点看,JAVA线程绝不是纯粹的用户级线程。
  • 不是纯粹内核级线程
    如果使用纯粹的内核级线程,那么有关线程的所有管理工作都是内核完成的,用户程序中没有管理线程的代码。显然,JAVA线程库提供了大量的线程管理机制,因此JAVA线程绝不是纯粹的内核级线程。