隔离是核心
内核的首要目标是隔离——防 bug、防攻击。硬件提供用户/超级模式和虚拟内存作为基础。
MIT 6.1810 · 第 3 课
操作系统设计的核心目标是隔离(isolation)。本课探讨如何实现隔离:硬件支持(用户/超级模式、虚拟内存)、宏内核与微内核的权衡、以及 xv6 系统调用的实际实现。
操作系统由应用程序(sh、echo 等)、系统调用接口(open、read、fork 等)、内核(实现系统调用的代码)、以及底层硬件(CPU、RAM、磁盘)组成。严格来说,"操作系统"包含内核 + 用户态服务,而"内核"是其中运行在特权模式的核心部分。
如果应用程序直接与硬件交互——高效、灵活,但完全没有隔离:
| 问题 | 后果 |
|---|---|
| 资源隔离 | 一个程序耗尽内存、CPU 或磁盘,其他程序全部崩溃 |
| 内存隔离 | 一个程序的 bug 写入另一个进程的内存,破坏其状态 |
安全假设:
内核必须谨慎处理所有用户交互。任何内核 bug 都可能成为安全漏洞——这就是 security mindset。内核位于 可信计算基(TCB) 中。
两大支柱:
CPU 与内核协同设计:
| 机制 | 作用 |
|---|---|
| 用户/超级模式 | 超级模式可执行特权指令(访问设备、修改页表);用户模式不能 |
| 虚拟内存 | 页表将 VA → PA,限制进程可访问的内存;页表只在超级模式下可修改 |
应用程序运行在用户模式,但系统调用需要超级模式。RISC-V 提供 ecall 指令:
// 用户代码
open():
ecall SYS_open // 触发系统调用
// ecall 会:
// 1. 切换到超级模式
// 2. 跳转到内核已知的入口点
// 3. 内核接管执行
可以!使用强类型语言(如 Singularity OS)。但用户只能使用经过批准的编译器/语言,灵活性受限。目前主流仍是硬件用户/超级模式。
整个内核是一个大程序——xv6、Linux 都采用这种设计。
| 优点 | 缺点 |
|---|---|
| 子系统易于协作(共享缓存、直接函数调用) | 复杂度高、bug 多、内部无隔离 |
| 性能高(无 IPC 开销) | 一个模块崩溃可能导致整个内核崩溃 |
把大部分 O/S 功能移到用户态服务进程,内核只保留最小功能:地址空间、内存管理、线程、IPC。
| 期望收益 | 设计挑战 |
|---|---|
| 更小 → 更少 bug → 更安全(可形式化验证,如 seL4) | 最小功能集是什么?能否保证安全? |
| 更简单 → 更易优化 | 性能(IPC 开销)能否接受? |
| 用户态 → 更易扩展/替换/重启服务 | 如何让程序共享磁盘/网络? |
xv6 运行在 SiFive HiFive Unleashed 开发板上(QEMU 模拟为 virt 机器):
QEMU 是一个 C 程序,忠实地模拟硬件:
// 简化模型
uint64 reg[32];
for (;;) {
instruction = read_mem(pc);
decode(instruction);
execute(instruction); // e.g., add, ld, ecall
}
$ ls kernel/
bio.c -- buffer cache 层
console.c -- 控制台驱动
exec.c -- exec() 实现
file.c -- 文件描述符
fs.c -- 文件系统
kalloc.c -- 物理页分配
kernelvec.S -- 内核中断入口
main.c -- 启动初始化
pipe.c -- 管道
proc.c -- 进程管理
spinlock.c -- 自旋锁
start.c -- 机器模式入口
trap.c -- 中断/异常/系统调用
uart.c -- UART 驱动
vm.c -- 虚拟内存/页表
...
$ make # 编译内核 + 用户程序 + 文件系统镜像
$ ls -l kernel/kernel
$ more kernel/kernel.asm
$ ls -l fs.img
$ make qemu # QEMU 仿真运行
main() 初始化内核与硬件。userinit() 创建第一个用户进程,执行 exec("/init")。init 是顶级进程,打开控制台 fd 0/1,fork/exec 启动 shell。// user/usys.S 中的 exec
exec:
li a7, SYS_exec # 系统调用号
ecall # 进入内核
ret
// kernel/syscall.c
void syscall(void) {
num = p->trapframe->a7;
syscalls[num](); // → sys_exec()
}
// kernel/sysfile.c
uint64 sys_exec(void) {
// 获取参数(pathname, argv)
return kexec(); // → kernel/exec.c
}
// kernel/exec.c
int kexec(char *path, char **argv) {
// 1. 从磁盘读取 ELF 文件
// 2. 严格的防御性检查(防止恶意文件欺骗内核)
// 3. 分配新内存、加载代码和数据
// 4. 分配用户栈、写入 argv
// 5. 设置进程为新程序
}
用户程序可能故意构造恶意 ELF 文件。kexec() 必须检查:文件大小、魔数、段边界、栈溢出等。内核绝不能信任用户数据!
Q1:操作系统设计的核心目标是什么?
Q2:RISC-V 中,用户态(user mode)不能执行以下哪项操作?
Q3:宏内核(Monolithic Kernel)的主要缺点是什么?
Q4:QEMU 是什么?
Q5:微内核设计的主要目标是什么?
内核的首要目标是隔离——防 bug、防攻击。硬件提供用户/超级模式和虚拟内存作为基础。
假设用户代码恶意,内核必须防御性编程。任何内核 bug 都可能是安全漏洞。
宏内核性能高但复杂;微内核简洁但 IPC 开销大。现代 OS 是两种设计的混合体。
xv6 是一个小型的宏内核,代码量小但包含所有核心机制——是理解 OS 的理想案例。