1 分钟阅读 次阅读

概要:梳理用户态切换到内核态的三种触发场景(系统调用、硬件中断、程序异常),并详细拆解 CPU 硬件自动完成的五个步骤:查 IDT、特权级安检、切栈、压栈保存现场、跃迁切换。

什么时候需要从用户态切换到内核态

1. 主动的求助:系统调用 (System Call)

这是程序主动触发切换的情况。

  • 什么时候发生? 当普通程序(Ring 3)想要执行一些自己没有权限的操作,必须让内核(Ring 0)代劳的时候。
  • 常见场景
  • 屏幕输出/文件读写:代码中执行 printf("Hello"),或者用 Python 执行 file.read()。程序自身不能直接操控显示器或硬盘,因此必须进入内核,由操作系统代为向屏幕或硬盘写入数据。
  • 申请内存:在 C 语言中调用 malloc 申请大块内存,如果底层的缓存不足,程序就会进入内核向系统申请物理内存(例如触发 sbrkmmap 系统调用)。
  • 网络通信:无论是发起 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,从中读出 ESP0SS0,立即覆盖当前的 %esp%ss。此时,栈指针已经稳稳地指向内核空间。

④ 压栈保存现场 (硬件级 Trapframe 雏形)

  • 硬件动作:切换内核栈后,CPU 开始在内核栈上依次压入数据。注意,压栈顺序由硬件固定,不会出错
  1. 压入刚被替换的用户态 %ss
  2. 压入用户态 %esp(这两步保住了用户栈的位置)
  3. 压入当前的 EFLAGS(标志寄存器,保存各种状态位)
  4. 压入用户态代码段 %cs
  5. 压入下一条要执行的用户态指令地址 %eip
  • 补充细节:这 5 个寄存器构成了内核栈上 trapframe 最底部的基石。

⑤ 跃迁与切换

  • 硬件动作:把第 ① 步准备好的新 %cs%eip 写入寄存器。
  • 同时,CPU 将当前的 CPL 强制设为 0。如果是中断门,还会将 EFLAGS 中的 IF 标志位清零(屏蔽后续硬件中断)。
  • 下一时刻,CPU 开始执行 %eip 指向的第一条内核指令。

流程结束后的交接:从硬件到软件

第 ⑤ 步完成后,CPU 硬件的自动流程结束,控制权移交给操作系统的软件代码。

此时内核代码刚开始执行,面临的现场是:

  • 已经处于 Ring 0,使用安全的内核栈。
  • 栈上保存着硬件自动压入的 5 个关键寄存器。

但内核认为这还不够,因为 CPU 还有很多通用寄存器(如 %eax%ebx%ecx 等)没有被硬件保存。因此内核执行的第一批汇编指令,通常是一连串的 pushal(将所有通用寄存器压入内核栈)。

直到这一刻,一个完整的、包含所有寄存器状态的 trapframe 才算在内核栈上构建完毕。接下来,操作系统就可以处理系统调用的具体逻辑了。

留下评论