正如我们在CPU 虚拟化相关内容所说, CPU 要进行时分共享以实现虚拟化.
而操作系统在构建一个虚拟化机制的时候有两个挑战:

  • 性能: 如何不增加系统开销的情况下实现虚拟化?
  • 控制权: 如何在保留对 CPU 的控制权的同时有效运行进程?

这也需要操作系统利用硬件的支持来实现.

直接执行

字面意思, 只需要在 CPU 上运行程序即可.

当 OS 希望程序启动, 会在进程列表为其创建一个进程条目, 分配内存, 加载代码, 跳转至入口并运行.

是一种最基本的直接执行协议, 没有任何限制.

Direction Execution Protocol

・・ ・ー・・ ーーー ・・・ー ・ ー・ーー ーーー ・・ー

那么在此基础上的受限呢? 如何解决我们的两个问题?

受限制的操作

先回答第二个问题: 如何保证控制?

直接执行的优势是快速, 但是如果进程希望执行受限操作(例如请求系统资源, 或 I/O 请求)怎么办?

方法是, 引入处理器模式:

  • 用户模式(user mode): 运行的代码会受到限制, 例如不能发出 I/O 请求.
  • 内核模式(kernel mode): 操作系统(或内核)以此种模式运行, 可以进行特权操作.

这两种模式是通过硬件实现的.
并且几乎所有现代硬件都提供了用户程序执行系统调用的能力, 允许内核向用户程序暴露一些关键功能.

要执行这种系统调用, 程序要执行特殊的陷阱(trap)指令,
该指令同时跳入内核并将权限提升至内核模式, 系统也因此可以执行任何需要的特权操作, 为进程执行工作.
完成后, 操作系统调用另外一个特殊治疗, 从陷阱返回到发起调用的用户程序, 并且将特权降低到用户模式.

但是, 陷阱如何知道在 OS 内要运行什么代码?
要知道, 发动调用的过程不能指定要跳转到的地址(不然程序可以跳转到内核任意位置, 通过调用, 可以恶意执行任意程序序列或跳过权限检测, 十分危险!)

因此, 内核通过启动时设置陷阱表来实现.
当机器启动时, 在内核模式下运行, 操作系统所做的第一件事就是告诉硬件在发生某些异常事件的时候要运行哪些代码(跳转到那里).

Limited Direction Execution Protocol
(粗体指令为特权指令)

Note

每个进程都有一个自己的内核栈(分配于内核内存空间, 作为内核运行的栈空间),
在进行系统调用陷入内核后, 内核会把这个程序的内核栈作为自己的栈空间. 在进入离开内核的时候, 寄存器在这里(通用寄存器和 PC)被恢复保存.

LDE 协议有两个阶段:

  1. 系统引导时, 内核初始化陷阱表, 并且CPU 记住其位置以供随后使用.
  2. 运行进程时, 在使用陷阱返回指令开始执行进程之前, 内核设置一些内容(例如在进程列表分配一个节点, 分配内存). 这会将 CPU 切换到用户模式并且开始执行该进程. 而当进程要发出系统调用时, 会重新陷入内核, 然后从从陷阱返回, 控制权还给进程.
    该进程完成工作后, 从 main() 中返回, 返回到一些存根代码, 例如 exit 系统调用(浙江在此陷入内核), 将环境清理干净.

在进程间切换

接下来回答第一个问题, 如何高性能实现虚拟化. 也就是操作系统是如何实现进程间切换, 营造出无限 CPU 的假象, 并且不影响性能.

这也是直接执行机制面临的另一个问题.

试想, 如果有一个进程在 CPU 上运行, 那么操作系统就没有运行. 如果操作系统没有运行, 它怎么可能可以获取对计算机控制权并切换进程?

协作方式: 等待系统调用

这是比较老的操作系统采用的方式. 他们选择约定: 操作系统相信其他进程会合理运行, 进程运行到一定时间后会放弃 CPU,
以便于操作系统决定下一个运行的任务.

他们为此有特殊的系统调用, 专门用于把控制权交给操作系统; 如果应用进行了非法操作, 也会把控制权交给操作系统.

因此在这种系统中, 操作系统是一个被动的状态, 等待系统调用或者非法操作, 重新接管 CPU.

非协作方式: 操作系统控制

但是事实证明, 如果没有硬件帮助, 如果程序不遵守约定定期归还控制权, 也不进行非法操作, 那么在以上的模式中, 操作系统什么也不能做.

因此人们选择依靠硬件: 时钟中断.
时钟可以定期产生中断, 此时运行的进程会停止, 操作系统预先配置的中断处理程序会运行, 操作系统重新获得 CPU 控制权, 进行一些调度等.

所以操作系统在机器启动时, 还要:

  1. 告诉硬件, 哪些代码发生时钟中断时运行.
  2. 启动时钟.(当然, 后续也可以通过特权操作关闭)

中断时, 硬件和进行系统调用陷入内核时类似, 要保存必要的机器状态以便于过后恢复进程.

保存与恢复上下文

操作系统取回控制权后, 必须决定: 继续执行当前进程, 或切换到另一个进程. 这由调度程序做出决定.

如果切换的话, 操作系统会执行底层代码, 即上下文切换.
要做的就是为当前执行的进程保存一些寄存器的值(到这个进程的的内核栈), 为即将执行的程序恢复一些寄存器的值(从那个进程的内核栈).

内核执行汇编代码(switch() 例程)保存通用寄存器, PC, 当前进程的内核栈指针, 来保存当前的进程到 PCB;
切换内核栈, 恢复寄存器, 提供给将要运行的进程(并且让 CPU 觉得自己处理的是 B 的内核逻辑).

因此, 通过切换栈, 内核进入和返回的是两个不同进程的上下文, 在从陷阱返回后, 即将运行的程序就变成了运行的进程.

LDE(Timer Interrupt)

整个过程如图.

要注意的是, 协议中有两个不同类型的寄存器保存/恢复:

  1. 发生时钟中断的时候, 运行进程的用户寄存器硬件隐式保存, 使用该进程的内核栈.
  2. 操作系统决定从 A 切换到 B 的时候, 内核寄存器软件, 即操作系统, 显式保存与进程的 PCB 内存内.

其中, 第一个操作使得陷入内核后, 寄存器被内核覆盖使用后, 数据不会被覆盖消失.
第二个操作保存和切换的是在内核模式运行时的寄存器, 使得系统好像由从 A 陷入内核变成由 B 陷入内核.

也就是, 整个过程中, 程序的运行状态变化就是: A 用户态 -> A 内核态 -> B 内核态 -> B 用户态

并发问题

当系统在中断或陷阱处理中发生另外的中断, 操作系统要怎么办? 可能单纯禁止中断, 或者也有各种加锁方案.

后面会再次涉及.