用户态切换到内核态 —— 硬件的五个步骤
概要:梳理用户态切换到内核态的三种触发场景(系统调用、硬件中断、程序异常),并详细拆解 CPU 硬件自动完成的五个步骤:查 IDT、特权级安检、切栈、压栈保存现场、跃迁切换。
什么时候需要从用户态切换到内核态
1. 主动的求助:系统调用 (System Call)
这是程序主动触发切换的情况。
- 什么时候发生? 当普通程序(Ring 3)想要执行一些自己没有权限的操作,必须让内核(Ring 0)代劳的时候。
- 常见场景:
- 屏幕输出/文件读写:代码中执行
printf("Hello"),或者用 Python 执行file.read()。程序自身不能直接操控显示器或硬盘,因此必须进入内核,由操作系统代为向屏幕或硬盘写入数据。 - 申请内存:在 C 语言中调用
malloc申请大块内存,如果底层的缓存不足,程序就会进入内核向系统申请物理内存(例如触发sbrk或mmap系统调用)。 -
网络通信:无论是发起 HTTP 请求还是接收数据,底层的
send()和recv()函数都会触发这次切换。 - 动作:程序在汇编层面主动执行
int $64(xv6 中)或int $0x80(早期 Linux 中),即主动发起进入内核的请求。
2. 被动的打断:硬件中断 (Hardware Interrupt)
这是程序正常运行过程中,外部硬件突然打断的情况。
- 什么时候发生? 当主板上的外设硬件有紧急事务,需要 CPU 立刻停止当前程序去处理时。
- 常见场景:
- 时钟中断(操作系统多任务的基石):主板上的定时器每隔几毫秒发出一次中断信号。程序本身没有问题,只是运行时间过长。CPU 强制进入内核,内核发现该程序时间片已用尽,便把 CPU 切换给另一个程序使用。
- 网卡收发:电脑收到一条消息时,网卡把电信号转成数据包后,立刻发送信号打断 CPU。CPU 进入内核,把网卡中的数据取出放到内存中。
-
鼠标键盘:移动鼠标或按下
Ctrl+C时,硬件立刻触发中断,内核读取按键动作,决定是否终止当前程序。 - 区别注意:硬件中断是外部强制的,因此 CPU 会自动跳过五个步骤中的”② 特权级安检”,直接切栈保存现场。
3. 意外的翻车:程序异常 (Exception / Fault)
这是程序运行中犯了致命错误,被 CPU 硬件当场抓获的情况。
- 什么时候发生? CPU 执行指令时发现无法继续执行,只能强行呼叫内核来处理问题或终止程序。
- 常见场景:
- 除以零:代码执行了
a = b / 0。CPU 的算术逻辑单元(ALU)无法计算,直接触发硬件异常。进入内核后,内核查明原因,终止程序并在终端输出 “Floating point exception”。 - 段错误 (Segmentation Fault):编写 C 语言时指针指向错误位置,试图向只读内存区域写入数据,或访问不属于自己的内存地址。CPU 的内存管理单元(MMU)立即拦截,触发异常。内核接管后直接终止程序。
- 缺页异常 (Page Fault):程序访问了一个合法的虚拟地址,但该地址尚未对应真实的物理内存。CPU 触发异常,内核接管后立即在物理内存中分配一块空间,然后让程序退回刚才那条指令重新执行。程序本身并不知道自己经历了一次短暂的等待。
硬件的五个步骤
① 查 IDT 第 64 项
- 硬件动作:CPU 收到
int $64指令后,立即读取内部的IDTR寄存器,获得 IDT 表的物理基地址。然后计算偏移量(基址 + 64 * 8 字节),取出第 64 个门描述符。 - 提取目标:从描述符中取出内核代码段选择子(准备写入
%cs)和偏移地址(准备写入%eip)。
② 特权级安检 (CPL vs DPL)
- 硬件动作:CPU 发现这是一条软件发起的中断指令(
int指令),于是启动特权级检查。 - 将当前的 CPL(保存在当前
%cs的最低两位,此时为 3)与门描述符中的 DPL(3)进行比较。3 ≤ 3,检查通过。 - 补充细节:如果是外部硬件(如网卡)触发的中断,CPU 会跳过这个 DPL 检查直接放行。因为硬件中断不受用户程序控制,是绝对合法的。
③ 查 TSS 强制切栈
- 硬件动作:CPU 发现目标代码位于 Ring 0,发生了特权级跃迁(Ring 3 升 Ring 0)。
- 硬件规定,只要发生特权级跃迁,绝对不能再使用原来的栈。
- 于是 CPU 查询当前核心绑定的唯一 TSS,从中读出
ESP0和SS0,立即覆盖当前的%esp和%ss。此时,栈指针已经稳稳地指向内核空间。
④ 压栈保存现场 (硬件级 Trapframe 雏形)
- 硬件动作:切换内核栈后,CPU 开始在内核栈上依次压入数据。注意,压栈顺序由硬件固定,不会出错:
- 压入刚被替换的用户态
%ss - 压入用户态
%esp(这两步保住了用户栈的位置) - 压入当前的
EFLAGS(标志寄存器,保存各种状态位) - 压入用户态代码段
%cs - 压入下一条要执行的用户态指令地址
%eip
- 补充细节:这 5 个寄存器构成了内核栈上
trapframe最底部的基石。
⑤ 跃迁与切换
- 硬件动作:把第 ① 步准备好的新
%cs和%eip写入寄存器。 - 同时,CPU 将当前的 CPL 强制设为 0。如果是中断门,还会将
EFLAGS中的IF标志位清零(屏蔽后续硬件中断)。 - 下一时刻,CPU 开始执行
%eip指向的第一条内核指令。
流程结束后的交接:从硬件到软件
第 ⑤ 步完成后,CPU 硬件的自动流程结束,控制权移交给操作系统的软件代码。
此时内核代码刚开始执行,面临的现场是:
- 已经处于 Ring 0,使用安全的内核栈。
- 栈上保存着硬件自动压入的 5 个关键寄存器。
但内核认为这还不够,因为 CPU 还有很多通用寄存器(如 %eax、%ebx、%ecx 等)没有被硬件保存。因此内核执行的第一批汇编指令,通常是一连串的 pushal(将所有通用寄存器压入内核栈)。
直到这一刻,一个完整的、包含所有寄存器状态的 trapframe 才算在内核栈上构建完毕。接下来,操作系统就可以处理系统调用的具体逻辑了。
留下评论