Appearance
内核态与用户态
想象一下,如果任何一个程序都能直接动用电脑最核心的资源,那系统很快就会乱套,甚至崩溃。为了避免这种情况,操作系统设计了两种不同的运行模式:内核态 (Kernel Mode) 和 用户态 (User Mode)。
CPU 特权级别
这种模式划分的背后,是 CPU 硬件提供的特权级别支持。拿我们常用的 x86 架构 CPU 来说,它定义了 0 到 3 四个特权级别(常说的 Ring 0 到 Ring 3)。数字越小,权限越高。不过,主流的操作系统通常只用到两个级别:
- Ring 0 (内核态):最高特权级别。操作系统的核心代码和一些驱动程序就运行在这个级别,它们能直接操作硬件,拥有系统的完全控制权。
- Ring 3 (用户态):最低特权级别,用户应用程序运行在这个级别。它们不能直接执行一些敏感操作,比如直接访问硬件。
CPU 内部有一个特殊的寄存器(比如 x86 架构的 CS 段寄存器中的 CPL 位)会记录当前正在执行的代码属于哪个特权级别。
CPU 的指令集里,有一些指令被划分为特权指令,比如让处理器停止运行的 HLT 指令,或者关闭中断的 CLI 指令。这些指令只能在内核态 (Ring 0) 执行。如果一个用户态的程序斗胆尝试执行这些特权指令,CPU 硬件会立刻发现这个越权行为,并产生一个异常,强制程序的流程跳转到操作系统内核预设好的异常处理代码那里去。
模式切换
程序运行的时候,并不是一直固定在某一个模式下,它会在用户态和内核态之间切换。从用户态进入内核态,主要有下面三种途径:
- 系统调用 (System Call):这是最常见也是最正规的方式。当用户程序需要操作系统帮忙做一些它自己权限不够的事情时(比如读取文件、创建网络连接),就会发起系统调用,请求内核提供服务。
- 中断 (Interrupt):当外部硬件设备有事情需要 CPU 处理时(比如键盘被打了一下、鼠标点了一下,或者网卡收到了数据包),会发出中断信号。CPU 会暂停当前正在执行的用户程序,切换到内核态去处理这个中断。
- 异常 (Exception):当用户程序在运行过程中自己出了问题(比如除以零、访问了非法内存地址),或者遇到某些需要内核介入处理的情况(比如缺页),CPU 也会产生一个异常,并切换到内核态进行处理。
当内核处理完这些系统调用、中断或异常事件后,它会执行一条特殊的指令(比如 x86 架构中的 iret、SYSRET 或 SYSEXIT 命令)。这条指令会把 CPU 的特权级别从内核态切换回用户态,并让程序从之前被打断的地方继续执行下去。
中断与异常
中断 (Interrupt) 信号通常来自 CPU 外部,是硬件设备用来通知 CPU有事发生的一种方式。比如键盘按键、鼠标点击、定时器到点、网卡收到数据包等等,这些都会产生中断,请求 CPU 关注和处理。
因为电脑里外设很多,都可能产生中断,所以系统里通常会有中断控制器(比如经典的 8259A PIC 或者更现代的 APIC)。中断控制器负责接收各个设备的中断请求,排个优先级,然后统一向 CPU 发送中断信号。
操作系统在启动的时候,会在内存里建立一个叫做中断描述符表 (IDT)的表格。表里的每一项可以看作是一个中断向量或门描述符,指向一段特定的内核代码,这段代码就是中断服务例程 (Interrupt Service Routine, ISR),专门用来处理对应类型的中断。
异常 (Exception) 则不一样,它源于 CPU 内部,是 CPU 在执行当前指令时检测到错误或者发生了某些特殊情况。因为异常是指令执行的直接结果,所以它是同步发生的(即在指令执行过程中发生)。
常见的异常有这么几种:
- 故障 (Fault):通常是一些可以被纠正的错误。最典型的例子就是缺页故障 (Page Fault)。
- 陷阱 (Trap):通常是程序有意执行某条指令引发的,目的是主动切换到内核态。比如执行系统调用的特定指令,或者调试器设置的断点指令。
- 终止 (Abort):由非常严重的、通常无法恢复的硬件错误或系统内部错误引起的。
系统调用
系统调用是应用程序请求操作系统提供服务的标准接口。当一个用户程序发起系统调用时,大致会经历以下步骤:
- 准备参数与调用号:用户程序(通常是通过 C 库函数这样的封装层来实现)首先会把表示具体要调用哪个服务的系统调用号(比如
SYS_open/SYS_read)放进一个事先约定好的 CPU 寄存器里(比如 x86 架构的eax寄存器)。同时,也会把调用这个服务所需要的参数(比如文件名、读取长度等)放到其他约定的寄存器或者内存栈中。 - 触发陷阱指令:准备好之后,用户程序会执行一条特殊的陷阱指令。在传统的 x86 系统上,Linux 常用
INT 0x80指令,Windows 常用INT 0x2E指令。在现代 CPU 上,则有更高效的专用指令,比如 AMD 的SYSCALL和 Intel 的SYSENTER。 - 内核处理:CPU 捕获到这条陷阱指令后,就会根据这条陷阱指令在中断描述符表 (IDT) 中预设的入口点,跳转到内核代码中相应的处理程序。内核会根据之前传递过来的系统调用号,去执行对应的内核服务例程。
- 返回结果:内核的服务例程执行完毕后,会把执行的结果(比如成功读取的字节数,或者一个错误代码)也放到一个约定的寄存器里(比如还是
eax寄存器)。 - 返回用户态:最后,内核执行一条特殊的返回指令(比如
IRET、SYSRET或SYSEXIT),从刚才那条陷阱指令的下一条指令开始继续执行。
其实并不存在一条可以直接进入内核态的命令。模式的切换是伴随着特定指令(如 INT、SYSCALL)的执行而发生的。
在系统初始化阶段,操作系统会设置好全局描述符表 (GDT),GDT 中定义了内存中不同代码段和数据段的属性,其中就包括了它们的特权级别。内核代码所在的段,其特权级会被设置为 Ring 0。
当我们用户程序执行像 INT 这样的陷阱指令时,CPU 会查找 IDT,其条目会指明接下来应该执行的入口,同时指定目标代码段的选择子。CPU 通过这个选择子在 GDT 中找到对应的段描述符,发现其特权级别是 Ring 0,于是 CPU 在跳转到这段内核代码执行的同时,就会自动将当前的特权级别提升到 Ring 0,这样就完成了从用户态到内核态的切换。