操作系统通过时分共享(Time Sharing)(即轮流运行进程)来实现 CPU 虚拟化。
这一机制的核心挑战在于平衡高性能与控制权:OS 既要最小化虚拟化带来的开销,又要确保能有效管理资源,防止进程独占 CPU 或越权访问。
受限的直接执行
为了使程序尽可能快地运行,操作系统采用了 受限直接执行(Limited Direct Execution, LDE) 技术。“直接执行”意味着程序直接在 CPU 上运行。OS 只需完成进程初始化(如分配内存、加载代码、设置栈等),便跳转至程序的入口点开始执行。如下表所示:
| 操作系统 | 程序 |
|---|---|
| 在进程列表上创建条目 | |
| 为程序分配内存 | |
| 将程序加载到内存中 | |
根据 argc/argv 设置程序栈 | |
| 清除寄存器 | |
执行 call main() 方法 | |
执行 main() | |
从 main 中执行 return | |
| 释放进程的内存 | |
| 将进程从进程列表中清除 |
然而,这种简单的方法面临两个核心问题:
- 受限操作:如何在保证高效运行的同时,防止程序执行不被允许的操作?
- 控制权切换:操作系统如何暂停一个正在运行的进程并切换到另一个进程,以实现时分共享?
解决这些问题正是“受限”二字的由来——如果没有限制,操作系统将失去对机器的控制权,沦为一个普通的库。
受限制的操作
直接执行虽快,但若进程需要执行受限操作(如 I/O 请求或申请更多资源),直接运行会导致系统失控。
核心方案:受保护的控制权转移硬件通过两种执行模式保障系统安全:
- 用户模式(User Mode):应用程序运行于此,权限受限,无法执行特权指令或直接访问硬件。
- 内核模式(Kernel Mode):操作系统运行于此,拥有最高权限,可访问所有资源。
为了安全地执行特权操作(如读写磁盘),程序需使用 系统调用(System Call) 机制,其核心流程为:
- 陷入(Trap):程序执行
trap指令,CPU 提升权限至内核模式,并跳转至内核的陷阱处理程序。 - 内核处理:操作系统执行请求的操作(如
open()、read())。 - 返回(Return-from-Trap):内核执行特权指令,恢复寄存器并降级回用户模式,程序从陷阱后的指令继续执行。
系统调用为何像普通函数调用?C 库将系统调用封装为普通函数(如
open())。库函数内部通过汇编代码处理参数传递、执行trap指令以及处理返回值。对程序员而言,系统调用就像调用普通 C 函数一样简单。
陷阱表(Trap Table)
陷阱表(Trap Table),在某些架构中也被称为中断向量表(Interrupt Vector Table),本质上是硬件用于查找异常处理程序地址的映射表。
你可以把它想象成操作系统的“应急预案清单”或“内部电话簿”。当发生特定事件(如程序请求系统调用、发生除零错误或硬件中断)时,硬件不知道该如何处理,它只能去查这张表,找到对应的“处理程序”在哪里。
内核必须严格控制这些跳转的目标地址。如果允许用户程序指定跳转地址,恶意程序就可以让内核执行任意指令(例如跳转到内核中删除文件的代码片段),从而接管机器。因此,操作系统采用“预设入口”的策略:
- 启动时初始化:操作系统在启动时(内核模式)配置陷阱表,告知硬件发生异常(如系统调用、中断)时应跳转到的处理程序地址。
- 硬件记忆:硬件记住这些地址,直到机器重启。这意味着一旦机器启动,用户程序就只能通过这些被“官方认证”的入口进入内核。
进程切换
解决了直接执行的受限问题后,下一个核心挑战是:操作系统如何暂停当前进程并切换到另一个进程? 如果进程占用了 CPU,操作系统就无法运行,也就无法执行切换操作。这是获取 CPU 控制权的关键问题。
关键问题:如何重获 CPU 的控制权操作系统如何重新获得 CPU 的控制权(regain control),以便它可以在进程之间切换?
1. 协作方式:等待系统调用(Cooperative)
早期系统(如早期版本的 Macintosh 操作系统或 Xerox Alto 系统)采用此方式,依赖进程主动放弃 CPU。
- 主动让出:进程通过系统调用(如
yield、I/O 操作)将控制权交还给 OS。 - 异常处理:若进程执行非法操作(如除以零、非法访问内存),硬件会触发异常,将控制权转移给 OS。
- 致命缺陷:如果进程陷入死循环且不进行系统调用,OS 将无法重获控制权,只能重启机器。
2. 非协作方式:时钟中断(Timer Interrupt)
为了解决协作方式的缺陷,现代系统利用硬件机制——时钟中断。
- 机制:硬件每隔几毫秒产生一次中断,强制暂停当前进程,跳转到 OS 的预设中断处理程序。
- 效果:OS 周期性地重获 CPU 控制权,从而能够停止当前进程并调度其他进程。
硬件保障控制权时钟中断是操作系统维持机器控制权的根本保障,确保即使面对恶意或失控的进程,OS 仍能掌控全局。
3. 上下文切换(Context Switch)
当 OS 重获控制权并决定切换进程时,会执行上下文切换:
- 保存上下文:将当前进程的通用寄存器、PC 等保存到其内核栈或进程结构中。
- 恢复上下文:从下一个进程的结构中恢复其寄存器和内核栈。
- 切换栈:切换内核栈指针,使代码执行环境变为下一个进程的环境。
- 返回:执行
return-from-trap,CPU 加载新进程的上下文并开始执行。
下表展示了基于时钟中断的受限直接执行协议:第一阶段:启动
操作系统@启动(内核模式) 硬件 初始化陷阱表 记住以下地址:
系统调用处理程序
时钟处理程序启动中断时钟 启动时钟
每隔 x ms 中断 CPU第二阶段:运行
操作系统@运行(内核模式) 硬件 程序(应用模式) 进程 A…… 时钟中断
将寄存器(A)保存到内核栈(A)
转向内核模式
跳到陷阱处理程序处理陷阱
调用switch()例程
将寄存器(A)保存到进程结构(A)
从进程结构(B)恢复寄存器(B)
从陷阱返回(进入 B)从内核栈(B)恢复寄存器(B)
转向用户模式
跳到 B 的程序计数器进程 B…… xv6 的上下文切换代码# void swtch(struct context **old, struct context *new); # # Save current register context in old # and then load register context from new. .globl swtch swtch: # Save old registers movl 4(%esp), %eax # put old ptr into eax popl 0(%eax) # save the old IP movl %esp, 4(%eax) # and stack movl %ebx, 8(%eax) # and other registers movl %ecx, 12(%eax) movl %edx, 16(%eax) movl %esi, 20(%eax) movl %edi, 24(%eax) movl %ebp, 28(%eax) # Load new registers movl 4(%esp), %eax # put new ptr into eax movl 28(%eax), %ebp # restore other registers movl 24(%eax), %edi movl 20(%eax), %esi movl 16(%eax), %edx movl 12(%eax), %ecx movl 8(%eax), %ebx movl 4(%eax), %esp # stack is switched here pushl 0(%eax) # return addr put in place ret # finally return into new ctxt
上下文切换的耗时上下文切换和系统调用的耗时随着硬件性能提升而显著减少。例如,从 1996 年的微秒级(~6μs)提升至现代系统的亚微秒级。但需注意,内存密集型操作的性能提升幅度不如处理器速度显著。
担心并发吗
如果系统调用期间发生时钟中断,或者处理一个中断时发生另一个中断,会发生什么? 这确实是操作系统需要解决的复杂问题。操作系统通常采用以下策略:
- 禁用中断(Disable Interrupts):在处理中断期间,暂时屏蔽其他中断,确保当前处理过程的原子性。但需谨慎使用,过长时间禁用可能导致中断丢失。
- 锁机制(Locking):为了支持多处理器并发访问内核数据结构,操作系统引入了复杂的锁方案。
这正是本书第二部分“并发”将深入探讨的主题。
小结
本章介绍了实现 CPU 虚拟化的关键机制:受限直接执行(Limited Direct Execution, LDE)。 其核心思想类似于“婴儿防护(Baby Proofing)”:
- 设置限制:在启动时配置陷阱表和时钟中断,确保危险操作(如直接访问硬件)被禁止。
- 自由运行:在受限模式下让进程直接在 CPU 上高效运行。
- 介入干预:仅在进程请求特权操作(系统调用)或独占 CPU 时间过长(时钟中断)时,操作系统才介入接管控制权。
重启的价值在协作式调度中,面对死循环进程,重启(Reboot) 往往是唯一解。实际上,重启是构建健壮系统的有效工具:它能将软件重置为已知状态,回收泄漏资源,且易于自动化。在大型分布式系统中,定期重启服务是一种常见的运维策略。
至此,我们掌握了 CPU 虚拟化的底层机制。接下来的问题是:在特定时间,应该运行哪个进程? 这将是下一章“调度”的主题。