Skip to content

6.1810 2024 第3讲:操作系统设计

操作系统设计概览

  • OS 蓝图:
    • 应用层 (apps): sh, echo, 等。
    • 接口层: 系统调用 (open, read, fork, ...)。
    • 内核层 (kernel): 实现系统调用,管理硬件。
    • 硬件层: CPU, 内存 (RAM), 磁盘。

核心概念:隔离 (Isolation)

隔离是设计一个受保护内核的主要原因。想象一个没有操作系统的设计,应用程序直接与硬件交互。虽然高效、灵活,但主要问题是缺乏隔离:

  • 资源隔离: 一个应用可能耗尽内存、独占CPU或占满磁盘空间,影响其他应用。
  • 内存隔离: 一个应用的bug可能会意外写入另一个应用的内存空间,导致崩溃或数据损坏。

UNIX 系统调用接口通过抽象硬件资源来帮助实现隔离:

  • fork() 和进程 抽象了CPU核心,允许OS在多个进程间透明地切换,实现分时复用。
  • exec()/sbrk() 和虚拟地址 抽象了物理内存,使得每个进程都拥有自己独立的地址空间。
  • 文件 抽象了磁盘块,由OS负责布局和权限控制。
  • 管道 抽象了进程间的内存共享。

安全模型

  • 假设用户代码是恶意的: 内核必须防御性地处理所有来自用户空间的请求,因为任何内核bug都可能成为安全漏洞。
  • 假设内核代码是可信的: 内核开发者被认为是善意且有能力的,内核内部子系统之间通常不设防。

硬件层面的隔离机制

CPU和内核是协同设计的,提供了实现隔离的基础:

  1. 用户/监控模式 (User/Supervisor Mode):

    • 监控模式: 可以执行所有“特权”指令,如访问硬件、修改页表等。
    • 用户模式: 不能执行特权指令。
    • 内核运行在监控模式,应用程序运行在用户模式。
  2. 虚拟内存 (Virtual Memory):

    • 页表 (Page Table): 将虚拟地址(VA)映射到物理地址(PA),限制了进程可以访问的内存范围。
    • 每个进程都有自己的页表,由内核设置和管理,只能在监控模式下更改。

系统调用是如何工作的?

应用程序需要一种受控的方式来请求内核服务。ecall 指令就是这个机制:

  1. 切换到监控模式。
  2. 跳转到内核中一个预定义好的入口点。

这确保了控制权安全地转移给内核,内核在处理完请求后,再将控制权交还给应用程序。我们将在后续课程中深入探讨这个过程。

内核设计哲学:单体 vs. 微内核

  • 单体内核 (Monolithic Kernel):

    • 内核是一个单一的、巨大的程序,实现了所有系统调用(xv6, Linux)。
    • 优点: 子系统间易于协作,效率高。
    • 缺点: 内部交互复杂,容易产生bug,一个驱动的bug可能导致整个系统崩溃。
  • 微内核 (Microkernel):

    • 内核只提供最基本的服务(IPC、内存管理、进程管理)。
    • 其他服务(文件系统、网络、驱动)作为普通的用户进程运行。
    • 优点: 模块化,更健壮(服务可重启),潜在更安全(特权代码少)。
    • 缺点: 性能可能是个问题,因为服务间通信需要通过IPC,开销较大。

xv6 启动过程概览

  1. QEMU 加载: make qemu 启动QEMU,它模拟一个RISC-V计算机,将内核加载到内存地址 0x80000000 并开始执行。
  2. entry.S: 内核的第一条指令。在机器模式(M-mode)下设置初始环境,特别是为C代码准备一个栈。
  3. start.c: 设置硬件(如中断),然后从M-mode切换到监控模式(S-mode),并跳转到 main 函数。
  4. main.c: 内核的主函数。进行各种子系统的初始化,例如内存分配器 (kinit)、进程表、文件系统等。
  5. userinit(): 创建第一个用户进程 init
    • allocproc(): 分配一个 proc 结构体和内核栈。
    • 设置页表,并将 initcode.S 的二进制码复制到进程的内存中。
    • 设置进程的初始状态,包括程序计数器(epc)指向 initcode 的起始地址,栈指针(sp)指向用户栈顶。
    • 将进程状态设为 RUNNABLE
  6. 调度器 (scheduler) 找到 init 进程并运行它。
  7. init 进程执行的第一件事是 exec("/init", ...) 系统调用,加载并执行 /init 程序。
  8. /init 程序(user/init.c)创建控制台设备文件,然后启动 shell (sh)。

至此,系统启动完成,用户可以在shell中输入命令。