知识全景图/ 软件工程与系统/ 如何设计系统/ 计算机系统底座/ 如何设计操作系统

如何设计操作系统

从最小机器模型出发,看内核怎样被一连串失败逼出来。

阅读定位:这页不把操作系统写成名词表,也不复刻 Linux 教材目录。它只回答一个问题:如何从一台裸机器,设计出一个能运行多个相互隔离用户程序的最小 ToyOS。

写法规则:每个机制都必须有出生原因:硬件事实是什么,初始做法怎样坏掉,OS 被迫设计什么对象,最后用什么实验验收。
最小机器模型 进入内核 运行用户程序 夺回 CPU 隔离内存 托管硬件 管理资源账本 处理中途崩溃 验收 ToyOS
一、分阶段拆页

总页只负责回答“OS 为什么会长出来”。子页按失败链路逐步拆,不一次性铺成教材目录;下面正文只保留压缩预览,细节进入对应子页。

子页解决的失败为什么拆它
如何设计裸机启动与硬件抽象层内核代码在磁盘上,CPU 不会自动进入它先回答 OS 运行在哪里、怎样拿到机器控制权
如何设计进程与调度器用户程序死循环后,内核拿不回 CPU这是 OS 从“能运行程序”走向“能管理程序”的第一步
如何设计虚拟内存与地址空间用户程序乱写地址,破坏内核或其他进程没有内存隔离,就没有真正的多程序系统
如何设计系统调用与内核边界用户程序想用资源,但不能直接碰硬件和内核对象把用户态请求收成可检查、可失败、可回滚的内核入口
如何设计文件系统磁盘只认识块,用户却需要路径、文件、权限和恢复把块设备变成有名字、有身份、有引用关系的资源账本
如何设计设备驱动与中断硬件事件不会按用户程序节奏发生把设备事件收进内核状态机,并唤醒等待进程
如何设计 IPC 与同步进程隔离后需要协作,共享状态又容易写乱把消息传递、等待唤醒和共享状态修改收进内核协议
如何设计网络与 Socket网卡收到包,但用户程序不能直接抢包把网络连接收成受状态机、缓冲区和 fd 管理的资源
如何设计资源限制与故障恢复一个坏程序可能耗尽进程、内存、fd、磁盘和网络缓冲给每个资源建立主人、上限、释放路径和失败恢复动作
二、操作系统运行在什么机器上

先补硬件,但只补 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 }从内核切到用户态时要恢复寄存器状态
切到用户态设置低权限标志,跳转到用户入口用户程序不能直接执行内核特权操作
最小验收:运行一个只会打印并退出的用户程序;它能从用户态进入,最后通过系统调用回到内核。
五、第一个灾难:用户程序不归还 CPU
坏场景:用户程序执行 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 是同一个大问题:状态变化进行到一半失败,系统怎样知道该继续、回滚,还是修复。
十、ToyOS 第一版验收实验
实验通过标准证明了什么
启动实验从 bootloader 进入内核,打印 hello kernel机器控制权已经交给内核
用户态实验用户程序能运行、打印、退出内核能创建并托管用户程序
抢占实验死循环程序不能阻止其他程序运行定时器中断、调度、上下文切换成立
内存隔离实验用户程序写内核地址被拒绝,系统不崩地址空间、页表权限、异常处理成立
系统调用实验非法 fd、非法指针、无权限路径都返回错误系统调用边界没有默认信任用户输入
资源限制实验进程、fd、内存超过上限时只影响违规程序OS 有资源账本和隔离边界
崩溃恢复实验写文件中途断电后,重启能恢复一致状态文件系统开始具备恢复能力
十一、回到主线
裸机启动与硬件抽象层
如果你想下钻“内核怎样被启动、怎样接中断和设备”,继续看 如何设计裸机启动与硬件抽象层
进程与调度器
如果你想下钻“程序失控时,内核怎样夺回 CPU”,继续看 如何设计进程与调度器
虚拟内存与地址空间
如果你想下钻“用户程序乱写内存时,系统怎样隔离破坏”,继续看 如何设计虚拟内存与地址空间
系统调用与内核边界
如果你想下钻“用户程序怎样安全请求内核服务”,继续看 如何设计系统调用与内核边界
文件系统
如果你想下钻“磁盘块怎样变成文件、目录、fd 和崩溃恢复”,继续看 如何设计文件系统
设备驱动与中断
如果你想下钻“硬件事件怎样进入内核并唤醒进程”,继续看 如何设计设备驱动与中断
IPC 与同步
如果你想下钻“隔离后的进程怎样协作、怎样等待和唤醒”,继续看 如何设计 IPC 与同步
网络与 Socket
如果你想下钻“网卡包怎样变成用户程序可读写的 fd”,继续看 如何设计网络与 Socket
资源限制与故障恢复
如果你想下钻“坏程序和失败路径怎样只局部破坏”,继续看 如何设计资源限制与故障恢复
如何设计系统
如果你想把操作系统放回语言、编译器、数据库这一整条系统构造线,回到 如何设计系统
计算机系统底座
如果你还没建立硬件、运行时、内核、文件、网络之间的层级关系,先看 计算机系统底座
操作系统内核机制底座
如果你想把 ToyOS 对应到 Linux 的调度、虚拟内存、VFS、epoll、TCP、cgroup 和 namespace,继续看 操作系统内核机制底座
体系结构与编译运行时
OS 往下依赖 计算机体系结构,往上承接 编译器与运行时底座
总结:设计操作系统,不是先背进程、内存、文件系统这些词,而是先看到裸机器会怎样失控:程序不归还 CPU、乱写内存、直接碰硬件、抢占共享资源、写文件中途断电。内核就是为这些失败建立控制、隔离、接口、账本和恢复机制。