Appearance
第一章:操作系统概述
操作系统管理 CPU、内存和设备,为程序提供执行、通信和保存数据的接口。多个程序共享这些资源时,内核记录资源的归属与使用状态,协调并发访问,并借助硬件限制程序能够执行的操作。
操作系统的组成
先看一台运行 xv6 或 Linux 这类宏内核的机器。硬件、内核和用户程序承担不同职责。
硬件提供 CPU、内存和设备,并执行特权、地址翻译与中断等规则。内核维护资源状态,设置硬件规则,并向程序提供创建进程、读写文件等接口。用户程序借助这些接口运行;库、shell 和大多数系统服务也处于用户态。
内核内部按职责划分子系统。进程管理维护执行身份、资源引用与生命周期;调度分配 CPU;内存管理维护映射和物理页;文件与通信层组织持久数据或通信端点;驱动控制设备并处理请求完成。同步、引用计数和权限检查则贯穿各子系统。
用户程序与内核之间有硬件保护边界。内核内部的调度器、文件系统和驱动则通常共享内核地址空间,可以直接调用函数、访问共同对象。它们是职责分工,并不是各自受到硬件隔离的“小进程”。第十二章会比较把部分服务放进独立进程的微内核设计。
广义的操作系统还包含系统服务和工具。本讲义主要研究内核及其对应用提供的接口。shell 负责解释命令,但通常只是用户程序;它通过系统调用请求内核创建进程、打开文件。
用户代码、内核代码与设备的执行
在一个 CPU 核心上,某个时刻正在执行的可能是用户指令,也可能是内核指令。内核处理当前进程的系统调用时,通常使用该线程的内核栈;处理中断时,则处于中断执行上下文。Linux 还会让专门的内核线程执行回写等后台工作。
多核机器可以同时有一个核心执行用户代码,另一个核心执行内核代码,第三个核心处理设备中断。因此“CPU 在用户态还是内核态”必须针对具体核心、具体时刻来说。
设备也会独立工作。DMA 传输期间,CPU 可以执行无关程序。多个 CPU 核心与设备之间通过共享内存、请求队列和中断协调。
资源与抽象
应用通常不直接领取“CPU 的第几微秒”“内存条的第几个字节”或“磁盘的第几个扇区”。内核提供应用能够使用的对象和接口,再把它们落实到有限资源上。
| 硬件资源 | 应用看到的主要抽象 | 内核需要维护的关系 | 主要机制 |
|---|---|---|---|
| CPU 执行能力 | 可以暂停、恢复的执行流 | 执行现场、运行状态、CPU 归属 | 中断、调度、上下文切换 |
| 物理内存 | 地址空间 | 虚拟地址到物理页的映射与权限 | 页表、缺页处理、页分配 |
| 存储设备 | 文件与目录 | 名字到文件身份、文件偏移到数据块 | 路径查找、缓存、块分配、恢复协议 |
| 设备和通信通道 | 可通过接口操作的端点 | 打开引用、缓冲区、请求及完成状态 | 驱动、队列、通知与等待 |
进程把执行身份和多种资源引用组织在一起。文件描述符则是进程引用打开对象的一种方式。进程不是一个与 CPU、内存并列的新硬件部件,文件描述符也不是文件本身。
这些抽象同时承担两个任务:让程序容易使用机器,以及让多个程序共享机器时仍有明确的归属与保护规则。
内核对象
下面是 xv6 中一份简化的内存对象图。箭头表示引用或映射,方框之间的关系不表示调用顺序。
text
某 CPU 的状态 进程 A:struct proc
当前进程指针 ───────────────→ state / pid / parent
调度器 context trapframe ─────→ 保存的用户寄存器
context ───────→ 保存的内核切换现场
内核栈 ────────→ 内核调用链
pagetable ─────→ 页表 ─────→ 物理页
ofile[0..] ────→ struct file ──→ inode 或 pipe
cwd ───────────→ 当前目录 inode
进程 B:另一个 struct proc
独立的现场与页表
某些 ofile 条目 ─────────────────────────────→ 同一个 struct file这些对象之间包含三类关系。
执行状态回答“接下来从哪里继续”。寄存器里有指令位置、栈指针和计算中间值;暂停执行时,要把恢复所需的信息保存在内存里。用户现场与内核切换现场用途不同,第二、五章会分别展开。
资源引用回答“这个执行实例正在使用什么”。进程中的 FD 表引用打开文件对象,打开文件对象又引用 inode 或管道。两个进程可以引用同一个对象,因此“进程不同”不意味着所有资源都复制了一份。
映射关系回答“程序使用的标识怎样找到实际资源”。虚拟地址要经页表解释,整数 FD 要经当前进程的 FD 表解释,路径要经目录查找解释。它们都是间接关系,但负责的名字空间和权限不同,不能混成一种机制。
Linux 的结构会进一步拆分:每个线程有自己的任务对象,通过指针引用可能共享的地址空间、FD 表和凭证。xv6 把许多状态放在同一个 struct proc 中,是因为它的模型更简单。
内核数据与内核代码
内核对象大多是 RAM 中的数据结构。页表也是内存中的数据,只是硬件 MMU 会按约定格式读取它;进程表则主要由内核代码管理。一个进程不占用 CPU 时,它的状态和资源引用依然存在。
CPU 执行内核代码,内核代码读写这些对象以改变系统状态。例如,调度器在当前 CPU 上读取进程状态,选定可运行进程,再恢复它的执行现场。
特权切换、上下文切换与地址空间切换
“进入内核”“换进程”“换页表”经常相邻发生,但不是同一件事。
| 事件 | 改变什么 | 不必随之发生什么 |
|---|---|---|
| 系统调用或异常进入内核 | 当前 CPU 的执行权限与指令位置,并保存必要现场 | 不必换成另一个进程 |
| 上下文切换 | 当前执行流及其寄存器、栈等现场 | 同一进程的线程间切换不必更换地址空间 |
| 切换地址空间 | CPU 用来解释虚拟地址的映射上下文 | 不等同于选择下一个执行流 |
例如,查询 PID 的系统调用通常可以进入内核后马上返回,进程身份不变。阻塞读取则可能先进入内核,再因数据未到而切换执行流。xv6 在用户和内核入口处更换页表,但这时仍可能在服务同一个进程。
还有一个常见混淆:普通地址翻译由 MMU 完成,通常不会执行一遍内核代码。只有翻译或权限检查失败等情况才产生异常,由内核处理。
子系统协作
子系统通过函数调用、对象引用和事件通知协作:
| 发起方 | 协作对象 | 交接的内容 | 必须维持的关系 |
|---|---|---|---|
| 进程管理 | 内存管理、文件层 | 创建、复制、释放映射和引用 | 退出后不再使用的资源最终回收 |
| 内存管理 | 调度、存储 | 文件缺页所需 I/O;等待与恢复 | 页面准备好后再重试访问(Linux 场景) |
| 文件系统 | 缓存、驱动 | 块号、缓冲区、读写请求 | 正确的数据送到正确对象,错误能传回 |
| 设备完成处理 | 等待者与调度器 | 完成状态、唤醒 | 完成后可运行,不等于立刻运行 |
| 所有共享对象 | 同步机制 | 锁、原子更新、等待协议 | 并发访问不破坏对象不变量 |
文件读取示例
进程通过 FD 请求读取一个普通文件。文件层先找到打开对象,再按文件偏移查找数据。若缓存命中,可以复制数据并返回;若未命中,就要建立设备请求,并让当前执行流等待。设备完成后,驱动更新请求状态并唤醒等待者,调度器稍后恢复它,它才有机会继续读并返回用户态。
在一次读取中,FD 属于引用关系,文件偏移属于打开对象,缓冲区属于缓存,设备请求属于 I/O 路径,睡眠与就绪属于执行状态。它们参与同一次读取,却没有变成同一个对象。
如果把普通文件换成管道,FD 接口仍可使用,但数据来自另一进程写入的内核缓冲区,不必经过磁盘。同样的 read 接口会根据打开对象的类型,调用不同的实现。
运行时状态与持久数据
运行中的 OS 主要依赖 RAM 中的状态,而文件系统还维护设备上的长期结构。需要区分它们的寿命。
| 状态 | 通常存放在哪里 | 掉电后的处理 |
|---|---|---|
| 当前寄存器、进程表、内核栈 | CPU / RAM | 消失,启动后重新建立 |
| 页表、FD 表、等待队列 | RAM | 消失,不能靠磁盘上的程序文件直接恢复 |
| 文件缓存、未完成设备请求 | RAM 或设备易失缓存 | 可能丢失,不能仅凭 write 返回判断已保存 |
| 文件数据、目录、磁盘元数据、日志 | 已到达持久化边界的存储 | 重启后根据文件系统规则读取与恢复 |
磁盘上有一个程序文件,不代表重启后自动恢复了之前那个进程;文件数据已经持久化,也不代表此前的锁、FD 和执行现场仍在。虚拟机快照等机制会额外保存部分运行状态,属于第十一章讨论的更强协议。
缓存中的修改需要经过写回与同步操作,才能满足持久化要求;重启后的恢复则依赖磁盘上的数据结构与更新协议。第八、九章分别讨论这些机制。
内核启动与初始化
上述结构不是机器上电时天然存在的。启动代码需要建立可用的内核执行环境,初始化物理内存分配、页表、trap 入口和设备,准备调度所需的每 CPU 状态,然后创建最初的用户进程。
之后系统进入持续的运行阶段:用户代码直接执行,系统调用、异常和中断让 CPU 进入内核;内核维护对象、处理事件,选择合适的执行流继续。多核和设备可以并行推进,没有一个单一循环串行解释整台机器上的所有事情。
在 xv6 中,启动与初始化可从 kernel/entry.S、start.c、main.c 读起。这些代码负责在用户程序开始运行之前准备上述状态。
接口、机制与策略
同一项系统功能包含不同层次的设计:
- 抽象与接口:外部使用者能请求什么,得到什么承诺。
- 机制:状态保存在哪里,硬件和软件怎样改变它。
- 策略:在多个合法选择中选哪个,例如挑选哪个可运行线程、回收哪一页。
- 约束:哪些访问必须拒绝,哪些顺序不能颠倒,资源何时能释放。
例如,调度策略可以选择轮转或加权公平,但都需要保存与恢复执行现场的切换机制。改变策略会影响哪个进程先运行,不改变已阻塞进程必须等条件满足后才能继续的约束。
练习
- 两个进程共享一个物理页和一个打开文件,又轮流使用一个 CPU。哪些状态必须分别保存,哪些对象可以共用?
- 一个进程进入内核处理系统调用时,哪些状态发生变化,哪些身份和引用可以保持不变?
- 一个线程睡眠后,谁保存它的现场,谁记录它在等什么,谁使它重新可运行,谁决定它何时执行?
- 假如设备驱动被移到用户进程,哪些保护边界、调用和共享关系必须改变?
- 机器掉电后,哪些运行时对象消失,哪些持久数据需要被恢复?