Appearance
6.1810 2024 第3讲:操作系统设计
操作系统设计概览
- OS 蓝图:
- 应用层 (apps):
sh,echo, 等。 - 接口层: 系统调用 (
open,read,fork, ...)。 - 内核层 (kernel): 实现系统调用,管理硬件。
- 硬件层: CPU, 内存 (RAM), 磁盘。
- 应用层 (apps):
核心概念:隔离 (Isolation)
隔离是设计一个受保护内核的主要原因。想象一个没有操作系统的设计,应用程序直接与硬件交互。虽然高效、灵活,但主要问题是缺乏隔离:
- 资源隔离: 一个应用可能耗尽内存、独占CPU或占满磁盘空间,影响其他应用。
- 内存隔离: 一个应用的bug可能会意外写入另一个应用的内存空间,导致崩溃或数据损坏。
UNIX 系统调用接口通过抽象硬件资源来帮助实现隔离:
fork()和进程 抽象了CPU核心,允许OS在多个进程间透明地切换,实现分时复用。exec()/sbrk()和虚拟地址 抽象了物理内存,使得每个进程都拥有自己独立的地址空间。- 文件 抽象了磁盘块,由OS负责布局和权限控制。
- 管道 抽象了进程间的内存共享。
安全模型
- 假设用户代码是恶意的: 内核必须防御性地处理所有来自用户空间的请求,因为任何内核bug都可能成为安全漏洞。
- 假设内核代码是可信的: 内核开发者被认为是善意且有能力的,内核内部子系统之间通常不设防。
硬件层面的隔离机制
CPU和内核是协同设计的,提供了实现隔离的基础:
用户/监控模式 (User/Supervisor Mode):
- 监控模式: 可以执行所有“特权”指令,如访问硬件、修改页表等。
- 用户模式: 不能执行特权指令。
- 内核运行在监控模式,应用程序运行在用户模式。
虚拟内存 (Virtual Memory):
- 页表 (Page Table): 将虚拟地址(VA)映射到物理地址(PA),限制了进程可以访问的内存范围。
- 每个进程都有自己的页表,由内核设置和管理,只能在监控模式下更改。
系统调用是如何工作的?
应用程序需要一种受控的方式来请求内核服务。ecall 指令就是这个机制:
- 切换到监控模式。
- 跳转到内核中一个预定义好的入口点。
这确保了控制权安全地转移给内核,内核在处理完请求后,再将控制权交还给应用程序。我们将在后续课程中深入探讨这个过程。
内核设计哲学:单体 vs. 微内核
单体内核 (Monolithic Kernel):
- 内核是一个单一的、巨大的程序,实现了所有系统调用(xv6, Linux)。
- 优点: 子系统间易于协作,效率高。
- 缺点: 内部交互复杂,容易产生bug,一个驱动的bug可能导致整个系统崩溃。
微内核 (Microkernel):
- 内核只提供最基本的服务(IPC、内存管理、进程管理)。
- 其他服务(文件系统、网络、驱动)作为普通的用户进程运行。
- 优点: 模块化,更健壮(服务可重启),潜在更安全(特权代码少)。
- 缺点: 性能可能是个问题,因为服务间通信需要通过IPC,开销较大。
xv6 启动过程概览
- QEMU 加载:
make qemu启动QEMU,它模拟一个RISC-V计算机,将内核加载到内存地址0x80000000并开始执行。 entry.S: 内核的第一条指令。在机器模式(M-mode)下设置初始环境,特别是为C代码准备一个栈。start.c: 设置硬件(如中断),然后从M-mode切换到监控模式(S-mode),并跳转到main函数。main.c: 内核的主函数。进行各种子系统的初始化,例如内存分配器 (kinit)、进程表、文件系统等。userinit(): 创建第一个用户进程init。allocproc(): 分配一个proc结构体和内核栈。- 设置页表,并将
initcode.S的二进制码复制到进程的内存中。 - 设置进程的初始状态,包括程序计数器(
epc)指向initcode的起始地址,栈指针(sp)指向用户栈顶。 - 将进程状态设为
RUNNABLE。
- 调度器 (
scheduler) 找到init进程并运行它。 init进程执行的第一件事是exec("/init", ...)系统调用,加载并执行/init程序。/init程序(user/init.c)创建控制台设备文件,然后启动 shell (sh)。
至此,系统启动完成,用户可以在shell中输入命令。