Mechanism: Limited Direct Execution (受限直接执行)

课件:[[第3次课-06. Mechanism_Limited_Direct_Execution.pptx]]
教材:OSTEP Chapter 6

上一章我们知道了“进程”这个抽象,但要让多个进程轮流运行,光有抽象还不够,还需要一套具体机制。
本章要解决两个问题:

  1. 性能(Performance):CPU 虚拟化怎样做,额外开销才不会太大?
  2. 控制(Control):程序高效运行时,操作系统怎么保证自己随时能拿回控制权?

OSTEP 的答案是:受限直接执行(Limited Direct Execution, LDE)
核心思路很朴素:平时让程序直接在硬件上跑,关键时刻再由硬件和内核接管。

1. 从“直接执行”说起

最理想的性能方案就是直接执行:OS 完成初始化后,让程序一路在 CPU 上跑。

课件给出的典型流程是:

  • OS 侧:创建进程项、分配内存、装入程序、设置 argc/argv、清寄存器;
  • 程序侧:执行 main(),返回后退出;
  • OS 侧:回收内存、移除进程项。

这种方案很快,但有个根本问题:如果不加限制,OS 会失去控制。
用户程序可能直接做危险操作,也可能死循环不交出 CPU。那时操作系统就退化成“普通库”,不再是管理者。

所以关键不是“要不要直接执行”,而是:如何在直接执行上加边界。

2. 问题一:受限操作怎么做?(Restricted Operation)

2.1 双模式:用户态与内核态

硬件先提供最基础的隔离机制:

  • User mode(用户态):应用不能直接访问全部硬件资源;
  • Kernel mode(内核态):内核可执行特权操作。

这样,像磁盘 I/O、进程创建、内存扩展这类操作,就不能让应用直接碰硬件,只能经内核代理。

2.2 系统调用与受保护控制转移

应用想要特权功能时,走系统调用路径:

  1. 执行 trap 指令,陷入内核;
  2. 硬件提升特权级,跳到内核处理例程;
  3. 内核完成检查与执行;
  4. 执行 return-from-trap,返回用户态继续运行。

这条路径的关键是“受保护控制转移(protected control transfer)”:
用户程序只能进入内核允许的入口,不能随意跳到内核任意地址。

2.3 Trap table 为什么在启动时初始化?

课件强调:OS 在 boot 阶段就初始化陷阱表(中断向量表),把系统调用和中断处理入口写给硬件。
之后每次 trap 或 interrupt,硬件按表跳转。这能防止用户程序伪造入口地址。

3. 问题二:OS 如何重新拿回 CPU?(Regaining Control)

只靠系统调用还不够,因为程序可能一直不调用系统调用。

3.1 协作式方案(Cooperative)

思路是“程序主动让出 CPU”:

  • 通过 yield() 等系统调用交还控制权;
  • 或发生非法操作(如除零、非法地址访问)触发异常,陷入内核。

问题是:若程序死循环且不触发系统调用,OS 就拿不回 CPU,只能重启。

3.2 非协作式方案(Non-Cooperative)

现代 OS 用硬件定时器解决这个问题:

  • OS 启动时编程 timer;
  • timer 每隔若干毫秒触发一次时钟中断;
  • 中断发生时,当前进程被硬件打断,状态被保存,CPU 强制转入内核处理程序。

这意味着:无论进程愿不愿意,OS 都能定期重新获得运行机会。

4. 上下文切换(Context Switch)

当中断或系统调用让内核重新运行后,调度器要决定:

  • 继续当前进程;还是
  • 切换到另一个进程。

若发生切换,核心工作是“保存旧上下文 + 恢复新上下文”。

4.1 需要保存/恢复哪些状态?

课件列出的重点包括:

  • 通用寄存器;
  • 程序计数器(PC);
  • 栈相关寄存器(尤其是内核栈指针)。

4.2 切换动作的本质

switch() 这段低层汇编通常做三件事:

  1. 把进程 A 的寄存器现场写入 A 的内核数据结构;
  2. 从进程 B 的内核数据结构读出寄存器现场;
  3. 把栈指针切到 B 的内核栈,然后返回。

“返回”之后,CPU 自然就会按 B 的现场继续执行。

5. 结合时钟中断看完整 LDE 协议

课件第 14、15 页给出的链路可以串成下面这条主线:

  1. boot 阶段:OS 初始化 trap table,并启动 timer。
  2. 用户态运行 A:A 正在 CPU 上执行。
  3. timer 中断到来:硬件保存 A 的必要状态,切入内核。
  4. 内核处理 trap:调度器决定切到 B。
  5. 调用 switch():保存 A、恢复 B、切换到 B 的内核栈。
  6. return-from-trap:硬件回到用户态,从 B 的 PC 继续执行。

所以“受限直接执行”并不是频繁模拟执行,而是:
大部分时间直接跑用户代码,少量关键时刻由硬件强制回到内核并完成切换。

6. xv6 switch 代码怎么看

课件给了 xv6 的 switch(struct context **old, struct context *new) 代码。读这段代码时抓两段就够:

  • 前半段:把当前寄存器写入 old 指向的上下文;
  • 后半段:从 new 指向的上下文恢复寄存器并切换栈;
  • 最后 ret:跳转到新上下文对应的位置继续执行。

这就是“上下文切换”的可执行版本。

7. 中断处理中又来中断怎么办?

课件最后提出并发问题:处理中断时如果又来一个中断,会不会把内核状态搞乱?

常见应对手段:

  1. 在关键中断处理区间临时关闭中断;
  2. 用锁机制保护内核共享数据结构。

本质上都是为了保证:内核状态更新可控且一致。

随堂复习自测

1)为什么“无限制直接执行”不可行?

因为用户程序可能越权访问硬件或不交还 CPU,OS 会失去控制。

2)系统调用是如何安全进入内核的?

通过 trap + 已初始化的 trap table 进行受保护控制转移,入口由硬件强制决定。

3)为什么现代 OS 必须依赖时钟中断?

因为它提供非协作抢回控制权的能力,保证 OS 能周期性运行调度逻辑。

4)上下文切换的最低必要动作是什么?

保存当前进程寄存器现场,恢复目标进程寄存器现场,并切换到目标进程内核栈。

5)LDE 的核心思想一句话怎么说?

让应用尽量直接在 CPU 上运行,但通过 trap、timer interrupt 和上下文切换机制确保 OS 始终可控。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐