用户态和内核态是操作系统的两种运行状态。
内核态:处于内核态的 CPU 可以访问任意的数据,包括外围设备,比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序,并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
用户态:处于用户态的 CPU 只能访问受限资源,不能直接访问内存等硬件设备,不能直接访问内存等硬件设备,必须通过「系统调用」陷入到内核中,才能访问这些特权资源。
在 CPU 的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃,比如:清空内存,修改时钟等。如果所有的程序代码都能够直接使用这些指令,那么很有可能我们的系统一天将会死 n 次。
所以,CPU将指令分为 特权指令 和 非特权指令 ,对于较为危险的指令,只允许操作系统本身及其相关模块进行调用,普通的、用户自行编写的应用程序只能使用那些不会造成危险的指令。
基于安全的考虑,CPU 提供了特权分级机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低。
操作系统根据 CPU 的特权分级机制,把进程的运行空间分为「内核空间」和「用户空间」,分别对应着上图中, CPU 特权等级的 Ring 0 和 Ring 3。
有三种方式程序会从用户态陷入内核态:
系统调用:这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。
异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程「主动」发起的,异常和外围设备中断则是「被动」的。
当程序需要访问硬件资源的时候,比如内存、硬盘等,就需要通过「系统调用」陷入到内核中,才能访问这些特权资源。系统调用可以理解为内核实现的函数,比如应用程序要通过网卡接收数据,会调用 Socket 的 read 函数。
程序在执行系统调用的过程中会从用户态切换到内核态,再从内核态切换到用户态,过程如下:
从用户态到内核态:当应用程序使用系统调用时,先将系统调用名称转换为系统调用号,接着将「系统调用号」和「请求参数」放到寄存器里,然后执行中断指令(int $0x80 指令),产生一个中断,CPU 陷入到内核态。
执行内核态逻辑:CPU 跳转到中断处理程序,先将当前用户态的寄存器(用户态的代码段、数据段、保存参数的寄存器)保存到内核数据结构,接着将系统调用号从寄存器里面取出来,最后根据系统调用号,在「系统调用表」中找到相应的系统调用函数进行调用,并将寄存器中保存的参数取出来,作为函数参数。
从内核态到用户态:执行完系统调用后,执行中断返回指令(iret 指令),将原来用户态保存的现场恢复回来,包含代码段、指令指针寄存器等。这时候 CPU 恢复到用户态,用户态进程恢复执行。
一次系统调用过程中的,会发生两次「 CPU 上下文切换」(所谓的 CPU 上下文就是 CPU 寄存器和程序计数器):
第一次 CPU 上下文切换是从用户态切换到内核态:CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。
第二次 CPU 上下文切换是从内核态切换到用户态: CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
Linux 系统中每个进程都有两个栈,分别是用户栈和内核栈,当应用程序运行在用户态的时候,就会使用用户栈,当应用程序运行在内核态的时候,就会使用内核栈。
内核态与用户态的相互切换,其中最重要的一个步骤就是用户栈和内核栈的切换。
1、用户栈到内核栈:
执行中断指令(int $0x80 指令),中断发生时,CPU 去一个特定的结构(比如 TSS)中,获取该进程的内核栈的地址信息,也就是内核栈的段选择子和栈顶指针(这两个东西是描述内核栈在内存的哪个地址空间),并分别送入 ss 寄存器和 rsp 寄存器,这时候 CPU 就指向了该进程的内核栈的栈顶位置了,这就完成了用户态到内核态的一次栈的切换。(*PS:如果不明白 ss 和 rsp 寄存器是干嘛的,可以看这篇文章:一个学渣对于stack的顿悟(1):从CPU的视角说起)
然后,IP 寄存器(指令指针寄存器)跳入中断服务程序开始执行,中断服务程序会把用户态的所有寄存器压入到内核栈中,如下图,CPU 自动地将用户态栈的段选择子 ss3,和栈顶指针 rsp3 都放到内核态栈里了。这里的数字 3 代表了 CPU 特权级,内核态是 0,用户态是 3。
2、内核栈到用户栈
当中断结束时,中断服务程序会从内核栈里将 CPU 寄存器的值全部恢复,最后再执行iret指令。
将 ss3/rsp3 都弹出栈,并且将这个值分别送到 ss 和 rsp 寄存器中,这时候 CPU 就指向了该进程的用户栈的栈顶位置了,这样就完成了从内核栈到用户栈的一次切换。
内核栈的 ss0 和 rsp0 也会被保存到前面所说的 CPU 的一个特定的结构(比如 TSS)中,以供下次切换时使用。