如何设计操作系统
从最小机器模型出发,看内核怎样被一连串失败逼出来。
阅读定位:这页不把操作系统写成名词表,也不复刻 Linux 教材目录。它只回答一个问题:如何从一台裸机器,设计出一个能运行多个相互隔离用户程序的最小 ToyOS。
写法规则:每个机制都必须有出生原因:硬件事实是什么,初始做法怎样坏掉,OS 被迫设计什么对象,最后用什么实验验收。
最小机器模型
进入内核
运行用户程序
夺回 CPU
隔离内存
托管硬件
管理资源账本
处理中途崩溃
验收 ToyOS
总页只负责回答“OS 为什么会长出来”。子页按失败链路逐步拆,不一次性铺成教材目录;下面正文只保留压缩预览,细节进入对应子页。
先补硬件,但只补 OS 设计刚好需要的部分。这里不是计算机组成原理,而是 ToyOS 的约束条件。
| 硬件事实 | 如果忽略它,会怎样坏 | 逼出的 OS 设计 |
| CPU 只会从某个地址取指执行 | 它不会自动理解“内核”“程序”“权限”这些概念 | 启动流程、内核入口、初始化代码 |
| CPU 可以被中断打断 | 没有外部打断,死循环程序可能永远占着 CPU | 定时器中断、调度器、上下文切换 |
| CPU 有特权级 | 所有代码都能执行危险指令,用户程序可以接管机器 | 用户态 / 内核态、系统调用、异常处理 |
| 内存只是地址到字节 | 硬件不天然知道哪个地址属于哪个程序 | 地址空间、页表、访问权限 |
| MMU 按页表翻译地址 | 没有映射和权限,程序互相写坏只是时间问题 | 虚拟内存、缺页异常、内核地址保护 |
| 磁盘只按块读写 | 硬件不懂文件名、目录、权限和崩溃恢复 | 文件系统、inode、目录、写入顺序 |
| 设备通过寄存器、中断、缓冲区交互 | 每个用户程序都直接碰设备,系统会失控且不可移植 | 驱动、设备抽象、文件描述符 |
初始问题
我们的内核一开始只是磁盘上的一段二进制。CPU 上电后不会主动找到它、理解它、保护它。第一步不是运行应用,而是让机器把控制权交给内核。
| 要设计的东西 | 最小版本怎么做 | 验收问题 |
| Bootloader | 找到 kernel image,把它加载到内存固定位置 | 能否把内核字节搬到预期地址 |
| Kernel entry | 设置栈,进入内核入口函数 | 能否稳定执行第一段内核代码 |
| 早期初始化 | 准备中断表、物理内存信息和最小输出能力 | 能否打印 hello kernel |
| 内核控制权 | 内核进入主循环,不再依赖 bootloader | 后续逻辑是否由内核接管 |
bootloader:
load kernel image to memory
jump kernel_entry
kernel_entry:
setup_stack()
setup_interrupt_table()
detect_memory()
print("hello kernel")
kernel_main()
失败前提:如果所有代码都和内核运行在同一权限、同一地址空间,用户程序不是“被 OS 管理的程序”,而是另一段可以破坏机器的内核代码。
| 设计动作 | 最小结构 | 为什么需要 |
| 加载用户程序镜像 | ProgramImage { text, data, entry } | 内核要知道用户程序从哪里开始执行 |
| 准备用户栈 | UserStack { top, argv } | 用户程序需要参数、局部变量和调用栈 |
| 建立初始执行现场 | TrapFrame { pc, sp, flags } | 从内核切到用户态时要恢复寄存器状态 |
| 切到用户态 | 设置低权限标志,跳转到用户入口 | 用户程序不能直接执行内核特权操作 |
最小验收:运行一个只会打印并退出的用户程序;它能从用户态进入,最后通过系统调用回到内核。
坏场景:用户程序执行 while (true) {}。如果没有外部打断,CPU 会一直执行它,内核再也没有机会运行。
| 为什么会坏 | 被迫设计出的机制 | 最小结构 |
| CPU 默认只执行下一条指令,不懂“公平” | 定时器中断 | Timer { interval, handler } |
| 中断回来后要知道当前是谁 | 进程对象 | Process { pid, state, registers } |
| 切走后还要能切回来 | 上下文保存 | Context { pc, sp, regs } |
| 多个可运行程序需要选择顺序 | 调度器 | RunQueue { ready_processes } |
验收实验:同时运行一个死循环程序和一个每秒打印一次的程序;死循环不应该让打印程序饿死。
坏场景:程序 A 写错地址,把程序 B 的数据或内核代码覆盖。系统不是返回一个错误,而是不可预测地坏掉。
| 为什么会坏 | 被迫设计出的机制 | 最小结构 |
| 内存不天然记录“这块属于谁” | 地址空间 | AddressSpace { mappings } |
| 虚拟地址需要映射到物理页 | 页表 | PageTable { virtual_page -> physical_page } |
| 有些页只能读、不能写、不能执行 | 页权限 | PageFlags { R, W, X, User } |
| 非法访问不能直接拖垮系统 | 缺页 / 保护异常 | Fault { address, reason, process } |
验收实验:用户程序访问内核地址时触发异常;内核能杀掉该进程或返回错误,但系统继续运行。
坏场景:用户程序想打印、读文件、收网络包。如果让它直接操作磁盘、屏幕或网卡,它就能绕过权限、破坏共享状态,也会被设备细节绑死。
| 用户想做什么 | OS 设计动作 | 安全检查 |
| 打印文字 | 提供 write(fd, buf, len) | fd 是否有效,buf 是否属于用户地址空间 |
| 打开文件 | 提供 open(path, flags) | 路径是否存在,权限是否允许 |
| 等待输入 | 让进程阻塞,设备中断后唤醒 | 等待条件是否正确,是否可被取消 |
| 访问网络 | 把 socket 也设计成 fd | 端口权限、缓冲区、连接数量上限 |
核心判断:系统调用不是普通函数调用,而是不可信用户程序向内核申请资源的安全边界。
OS 开始变成资源账本
多个程序同时存在后,问题不再是“能不能运行”,而是谁持有什么资源、谁能访问什么、什么时候释放、超额时怎么拒绝。
| 冲突场景 | 需要的账本对象 | 验收问题 |
| A 打开文件,B 删除文件 | inode、目录项、打开文件表、引用计数 | 文件名删除后,已打开 fd 是否仍有一致语义 |
| A 疯狂创建进程 | 进程表、父子关系、进程数限制 | 一个用户能否耗尽所有进程槽 |
| A 占满所有 fd | 每进程 fd 表、全局文件对象、上限 | fd 泄漏是否只影响该进程或该用户 |
| A 占满内存 | 物理页分配器、地址空间、内存限额 | 内存耗尽时,内核是否能返回错误而不是崩溃 |
| A 读不该读的文件 | 用户身份、权限位、路径检查 | 权限检查是否在系统调用边界完成 |
一句话:内核不是一堆底层代码,而是机器资源的裁判、账本和边界。
坏场景:目录项写了,inode 没写;数据块分配了,位图没写;文件大小改了,内容没落盘。重启后文件系统自己和自己矛盾。
| 失败类型 | OS 设计动作 | 最小验收 |
| 元数据只写了一半 | 写入顺序约束 | 重启后不会出现指向垃圾块的目录项 |
| 多个元数据必须一起变化 | 简单 journal | 恢复时能判断提交完成还是回滚 |
| 应用以为写完,实际还在缓存 | fsync 语义 | 调用 fsync 后断电,关键数据仍可恢复 |
| 恢复过程又失败 | 幂等恢复流程 | 重复 recovery 不会二次破坏状态 |
连接数据库:这里和 DBMS 的 WAL 是同一个大问题:状态变化进行到一半失败,系统怎样知道该继续、回滚,还是修复。
| 实验 | 通过标准 | 证明了什么 |
| 启动实验 | 从 bootloader 进入内核,打印 hello kernel | 机器控制权已经交给内核 |
| 用户态实验 | 用户程序能运行、打印、退出 | 内核能创建并托管用户程序 |
| 抢占实验 | 死循环程序不能阻止其他程序运行 | 定时器中断、调度、上下文切换成立 |
| 内存隔离实验 | 用户程序写内核地址被拒绝,系统不崩 | 地址空间、页表权限、异常处理成立 |
| 系统调用实验 | 非法 fd、非法指针、无权限路径都返回错误 | 系统调用边界没有默认信任用户输入 |
| 资源限制实验 | 进程、fd、内存超过上限时只影响违规程序 | OS 有资源账本和隔离边界 |
| 崩溃恢复实验 | 写文件中途断电后,重启能恢复一致状态 | 文件系统开始具备恢复能力 |
文件系统
如果你想下钻“磁盘块怎样变成文件、目录、fd 和崩溃恢复”,继续看
如何设计文件系统。
如何设计系统
如果你想把操作系统放回语言、编译器、数据库这一整条系统构造线,回到
如何设计系统。
计算机系统底座
如果你还没建立硬件、运行时、内核、文件、网络之间的层级关系,先看
计算机系统底座。
操作系统内核机制底座
如果你想把 ToyOS 对应到 Linux 的调度、虚拟内存、VFS、epoll、TCP、cgroup 和 namespace,继续看
操作系统内核机制底座。
总结:设计操作系统,不是先背进程、内存、文件系统这些词,而是先看到裸机器会怎样失控:程序不归还 CPU、乱写内存、直接碰硬件、抢占共享资源、写文件中途断电。内核就是为这些失败建立控制、隔离、接口、账本和恢复机制。