Lec 3 操作系统设计(隔离、进程与内核组织)
总览
本讲定位:从"为什么需要 OS?"出发,确立多路复用 / 隔离 / 交互三大需求;理解硬件特权级如何支撑强隔离;对比宏内核与微内核两种组织哲学;最后落到 xv6 的进程抽象、安全模型与启动流程。
思考题:进程调用 exec() / fork() 会发生什么?对一个已死 PID 先 kill 再 fork 会怎样?
0. 本讲脉络
三大需求(多路复用/隔离/交互) ──► 为什么要 OS(对比"库"方案)
│
▼
抽象物理资源 + 硬件特权级(M/S/U) ──► 强隔离的实现
│
▼
内核组织:宏内核 vs 微内核
│
▼
进程抽象(struct proc / 两个栈) ──► 安全模型 ──► xv6 启动流程1. 操作系统的三大需求
- 多路复用:多个程序并发运行(如同时跑编译器与编辑器,各为独立进程)。
- 隔离:某进程有 bug 或异常,不应影响无关进程。
- 交互:又不能完全隔离——进程有时需有意交互(如管道)。
2. 为什么需要操作系统?
一种替代方案是把系统调用(如 read)实现成库,应用直接链接、甚至定制,从而直接操作硬件、按需最优使用资源。这在嵌入式/实时系统确实存在。
这正是需要一个有强制力的 OS 来居中管理的理由。
3. 抽象物理资源 + 硬件特权级
实现隔离的关键思路:禁止应用直接访问敏感硬件,改为提供抽象服务。Unix 中应用不直接操作磁盘而是 open/read/close;CPU 调度由 OS 自动在进程间切换并保存/恢复寄存器(应用感知不到时间共享);内存通过 exec 构建而非直接操作物理内存(不够时换出到磁盘);进程交互多经文件描述符,隐藏实现细节。
要做到强隔离,必须保证:应用不能修改甚至读取内核的数据结构与指令;不能访问其他进程内存。CPU 用硬件特权级支撑这一点。
- 机器态:权限最高,CPU 启动时所处模式,用于上电初始化;xv6 仅短暂运行后即切到管理态。
- 管理态:可执行特权指令(开关中断、读写页表寄存器等)。
- 用户态:应用运行处。若用户态尝试执行特权指令,CPU 不执行,而是陷入 (trap) 到管理态的特殊代码,由其决定(通常是终止该应用)。
应用通过 ecall 发起系统调用:该指令把 CPU 从用户态切到管理态并跳到内核入口。切换后内核可校验参数(如传入地址是否属于该进程内存)、判断权限(如是否允许写该文件),再决定执行或拒绝。
4. 内核组织:宏内核 vs 微内核
核心设计问题:OS 的哪些部分应运行在管理态?
优点:实现方便(无需划分"需特权/不需特权"代码);模块协作容易(如文件系统与虚拟内存可共享磁盘块缓存)。缺点:庞大复杂,没人能完全理解模块间交互 → 易引入 bug,而内核 bug 尤其严重。

应用要读写文件就向文件服务器发消息(IPC)并等响应。内核接口只含少量底层功能(启动应用、发消息、访问设备硬件),从而简单、易理解、易验证。
现实中两者都广泛使用:Linux 是宏内核(部分功能如窗口系统在用户态),靠子系统紧密集成获得高性能;Minix、L4、QNX 是微内核,多见于嵌入式——其中 seL4(L4 后代)小到可被形式化验证,证明了内存安全等属性的正确性。(微内核的细节与性能论文见 Lec 4。)
5. 进程:隔离的基本单位
xv6 用页表(硬件实现)为每个进程提供独立地址空间,把虚拟地址映射为物理地址。每个进程的地址空间从虚拟地址 0 开始,布局为:
① 代码段(指令)
② 数据段(全局变量)
③ 栈
④ 堆(malloc,可动态增长)。
地址空间最顶部放两个特殊页(各 4096 字节):
- 跳板页 (trampoline)(进出内核的代码)与
- 陷阱帧页 (trapframe)(保存用户寄存器,详见 Lec 6)。

内核为每个进程维护一个 struct proc(在 kernel/proc.h),最重要的字段:p->pagetable(页表)、p->kstack(内核栈)、p->state(运行状态:allocated/ready/running/sleeping/exiting)。
5.1 为什么每个进程要有两个栈?
每个进程有用户栈与内核栈 (p->kstack)。用户态运行时用用户栈、内核栈空闲;进入内核(系统调用/中断)时切到内核栈执行内核代码,用户栈数据保留但不用。
因为这样即使用户程序破坏了自己的用户栈,内核仍能安全执行。内核绝不能信任用户内存(用户栈可能被写坏、指向非法地址、甚至根本不存在),所以内核必须有一块自己掌控、用户碰不到的栈。
6. 安全模型
OS 必须假设:用户态代码会尽最大努力破坏内核或其他进程。例如尝试解引用越界指针、执行用户态禁止的指令、读写控制寄存器、访问设备硬件、向系统调用传精心构造的参数诱导内核崩溃。
其余一切行为内核必须禁止——这是 OS 设计的"绝对要求"。对内核自身代码的假设则不同:通常认为内核由善意谨慎的程序员编写、无 bug、无恶意。
7. xv6 与启动流程
xv6 跑在 RISC-V(参照 "SiFive HiFive Unleashed" 板:4 核、每核 32 寄存器/ALU/MMU/控制寄存器/定时器、共享 128MB RAM 与设备、UART 控制台、磁盘、以太网),用 QEMU(-machine virt)模拟,磁盘用 virtio。内核代码结构简单(如文件系统在 kernel/fs.c,模块接口在 kernel/defs.h),make 构建出内核二进制与磁盘镜像 fs.img。
entry.S:_entry → start() → main() → 第一个用户程序 init。- 内核加载到
0x80000000(RAM 起始),此时处于机器态。 - 第一条指令跳到
entry.S的_entry:设栈,跳到start.c的start()。 start()完成硬件初始化、切换到管理态,跳到main()。main()中 core 0 做初始化、其他核等待。- 内存分配器在启动时通过
kinit()/freerange()把所有物理页(每页 4096 字节)加入空闲链表。 - 启动第一个用户程序
init(user/init.c),它再启动其他进程(如 shell);allocproc()为进程分配内存、建页表、置为可运行。
ELF 执行:exec() 从磁盘读 ELF 可执行文件,把代码/数据加载到用户空间,分配栈并设置入口点(epc)与新栈指针(sp)。ecall 触发系统调用后,内核读 a7 判断调用号并跳到对应处理函数。
8. 思考题与自测
kill(pid),之后 fork() 可能复用该 PID,会发生什么?)kill 作用于"当时"的那个 PID 所指进程;若该进程已不存在,kill 不影响任何活进程(通常返回错误)。之后 fork() 新建的进程可能恰好复用同一个 PID 号,但它是一个全新的进程,与先前被 kill 的进程毫无关系——这说明 PID 只是一个可回收的标识符,不是进程身份的本质。由此引申出真实系统的一类竞态:若程序"记住"了旧 PID 并延迟向它发信号,可能误伤复用了该号的新进程(PID-reuse race)。
自测清单:
- [ ] OS 的三大需求是什么?为什么"隔离"和"交互"看似矛盾却都需要?
- [ ] "把系统调用做成库"的方案缺陷是什么(协作式时间共享)?
- [ ] RISC-V 三种特权级各自能做什么?用户态执行特权指令会怎样?
- [ ] 宏内核与微内核的定义、优缺点;seL4 为何重要?
- [ ] 进程为什么要有用户栈和内核栈两个栈?
- [ ] 内核对用户进程的三条硬性约束。
- [ ] 复述 xv6 从 0x80000000 到
init的启动顺序。
参考资料
- 会解释内核设计动机和实现方式
阅读 xv6 内核的完整实现代码
kernel/proc.h
kernel/defs.h
kernel/entry.S
kernel/main.c
user/init.c
