受限直接执行
一、引言:虚拟化CPU的挑战
1.1 虚拟化CPU的目标
操作系统通过时分共享(time sharing)技术虚拟化CPU,使多个进程看似同时运行。基本方法是让进程轮流运行一段时间,切换到下一个进程。
1.2 关键问题
实现CPU虚拟化面临两大挑战:
- 性能:如何在不增加系统开销的情况下实现虚拟化?
- 控制权:如何高效运行进程,同时让操作系统保留对CPU的控制?
核心问题:如何高效、可控地虚拟化CPU?
操作系统需借助硬件支持,以高性能方式虚拟化CPU,同时确保系统控制权,避免进程无限运行或访问未授权资源。
二、受限直接执行(Limited Direct Execution, LDE)
2.1 基本思想
受限直接执行(LDE)是一种高效运行进程的技术:
- 直接执行:直接在CPU上运行用户程序,减少开销。
- 受限:通过硬件和操作系统机制限制进程行为,确保操作系统保留控制权。
2.2 直接执行流程
操作系统启动进程的步骤:
- 在进程列表中创建条目。
- 为程序分配内存。
- 从磁盘加载程序代码到内存。
- 设置运行时栈(包括
argc
和argv
)。 - 清除寄存器,调用
main()
,开始执行用户代码。 - 程序从
main()
返回后,操作系统释放内存,清理进程。
问题:
- 如何限制进程执行未经授权的操作(如I/O)?
- 如何在进程运行时切换到其他进程,实现时分共享?
三、问题1:受限制的操作
3.1 挑战
关键问题:如何让进程执行受限操作(如I/O)而不完全控制系统?
直接执行允许进程高效运行,但若无限制,进程可能:
- 绕过权限检查(如直接读写磁盘)。
- 执行特权操作,导致系统失控。
3.2 解决方案:用户模式与内核模式
硬件提供两种执行模式:
- 用户模式(user mode):限制进程访问硬件资源(如I/O请求被禁止,非法操作引发异常,可能终止进程)。
- 内核模式(kernel mode):操作系统拥有完全权限,可执行特权操作。
提示:受保护的控制权转移通过用户模式和内核模式的切换,配合陷阱(trap)和从陷阱返回(return-from-trap)指令,确保安全执行特权操作。
3.3 系统调用
用户程序通过系统调用执行特权操作(如访问文件、分配内存):
- 陷阱指令(trap):
- 程序执行陷阱指令,跳转到内核模式,运行内核代码。
- 硬件保存调用者寄存器(如程序计数器)到进程的内核栈(kernel stack)。
- 陷阱表(trap table):
- 内核在系统启动时设置,指定陷阱处理程序地址。
- 硬件根据陷阱表跳转到正确代码,防止用户指定任意地址。
- 从陷阱返回:
- 内核完成操作后,执行从陷阱返回指令,恢复寄存器,切换回用户模式,继续用户程序。
补充:系统调用为何像过程调用?
- C库隐藏陷阱指令,封装系统调用(如
open()
、read()
)。 - 库用汇编代码按内核约定设置参数和系统调用号,执行陷阱指令,处理返回值。
3.4 陷阱表设置
- 启动时:操作系统在内核模式下通过特权指令设置陷阱表,通知硬件处理程序地址。
- 安全性:用户模式禁止设置陷阱表,防止恶意程序接管系统。
思考:若用户可设置陷阱表,可能跳转到内核任意代码,执行未经授权操作,甚至控制机器。
3.5 LDE协议(表6.2)
- 启动阶段:
- 内核初始化陷阱表,硬件记住系统调用处理程序地址。
- 运行阶段:
- 操作系统创建进程,加载程序,设置栈。
- 从陷阱返回,切换到用户模式,跳转到
main()
。 - 进程调用系统调用,陷阱到内核,处理后返回。
- 程序退出(通过
exit()
),内核清理资源。
四、问题2:在进程之间切换
4.1 挑战
关键问题:操作系统如何重新获得CPU控制权以切换进程?
若进程在CPU上运行,操作系统未运行,无法主动干预。需要机制确保操作系统能定期接管CPU。
4.2 协作方式
- 方法:进程通过系统调用(如文件操作、通信)或显式
yield
调用主动放弃CPU。 - 非法操作:如除以零、非法内存访问,触发陷阱,操作系统接管并可能终止进程。
- 问题:若进程进入无限循环且不调用系统调用,操作系统无法干预,可能需重启系统。
提示:操作系统需处理不当行为(如非法操作),通常终止违规进程。
4.3 非协作方式:时钟中断
- 时钟中断(timer interrupt):
- 硬件每隔几毫秒触发中断,暂停当前进程,运行操作系统中断处理程序。
- 操作系统重新获得CPU控制权,可决定继续当前进程或切换。
- 设置:
- 启动时,操作系统设置中断处理程序地址,启动时钟(特权操作)。
- 硬件保存进程状态(如寄存器)到内核栈,类似系统调用。
- 安全性:用户模式禁止关闭时钟,确保操作系统控制。
提示:时钟中断是维持操作系统控制权的关键,即使进程不协作也能强制切换。
4.4 上下文切换
- 定义:操作系统保存当前进程状态,恢复另一进程状态,切换执行。
- 步骤:
- 保存当前进程寄存器(程序计数器、通用寄存器等)到其进程结构或内核栈。
- 恢复目标进程寄存器,切换内核栈(指向目标进程)。
- 执行从陷阱返回指令,运行目标进程。
-
xv6上下文切换代码(图6.1):
assembly swtch: # 保存旧进程寄存器 movl 4(%esp), %eax # 获取旧进程context指针 popl 0(%eax) # 保存指令指针 movl %esp, 4(%eax) # 保存栈指针 movl %ebx, 8(%eax) # 保存其他寄存器 ... # 加载新进程寄存器 movl 4(%esp), %eax # 获取新进程context指针 movl 28(%eax), %ebp # 恢复寄存器 ... movl 4(%eax), %esp # 切换栈 pushl 0(%eax) # 设置返回地址 ret # 跳转到新进程
-
寄存器保存类型:
- 硬件保存:中断时,硬件保存用户寄存器到内核栈。
- 软件保存:操作系统保存内核寄存器到进程结构。
4.5 切换协议(表6.3)
- 启动:初始化陷阱表,设置时钟中断处理程序,启动时钟。
- 运行:
- 进程A运行,时钟中断触发。
- 硬件保存进程A寄存器,进入内核模式。
- 操作系统调用
switch()
,保存A的寄存器到进程结构,恢复B的寄存器,切换栈。 - 从陷阱返回,运行进程B。
五、并发问题
5.1 潜在问题
- 中断嵌套:系统调用处理期间发生时钟中断,或中断处理中发生另一中断,可能导致内核状态混乱。
- 解决方法:
- 禁用中断:在中断处理期间临时禁止中断,但时间过长可能丢失中断。
- 加锁机制:保护内核数据结构,允许多处理器并发访问(详见并发章节)。
5.2 上下文切换性能
- 工具:
lmbench
可测量系统调用和上下文切换时间。 - 历史数据:
- 1996年(200-MHz P6,Linux 1.3.37):系统调用约4μs,上下文切换约6μs。
- 现代系统(2-3 GHz CPU):亚微秒级。
- 瓶颈:内存密集型操作受内存带宽限制,性能提升不如CPU。
六、扩展与总结
6.1 受限直接执行的核心价值
LDE通过直接执行和硬件限制结合,实现高效、可控的CPU虚拟化:
- 高效性:程序直接运行在CPU上,减少开销。
- 安全性:用户模式、陷阱表和时钟中断确保操作系统控制。
- 灵活性:支持系统调用和进程切换,满足多任务需求。
类比:类似“宝宝防护”(baby proofing),操作系统通过设置陷阱和中断,限制进程访问危险资源(如特权操作),确保安全运行。
6.2 设计哲学
- 硬件-软件协作:硬件提供模式切换、陷阱和中断,操作系统实现调度和上下文切换。
- 模块化:陷阱表和中断处理程序独立配置,便于维护。
- 健壮性:处理非法操作和非协作进程,确保系统稳定。
提示:重启的用处
- 重启可恢复系统到已知状态,回收泄露资源,自动化管理。
- 在集群服务中,定期重启提高可靠性。