第一章:操作系统导论 🧐
1.1 什么是操作系统?
【重点】 操作系统(Operating System, OS)是计算机系统中最核心的系统软件。我们可以从两个不同的视角来理解它。
1.1.1 顶层视角:作为应用程序接口 (API) 的抽象层 🌱
从应用程序开发者或用户的角度(“自上而下”)来看,操作系统是一个提供服务的软件层。
核心定义 📝
操作系统是介于应用程序和硬件之间的一个软件层。它为上层应用程序提供统一、便捷的应用程序接口(API),同时负责管理所有共享的硬件资源。
它的主要目标是简化硬件资源的使用。如果没有操作系统,应用程序员必须直接编写代码来操作每一个硬件设备,例如:
- 将设备指令载入特定的设备寄存器。
- 处理复杂的设备初始化和时序控制。
- 解析设备返回的各种状态码。
这样的编程方式极其繁琐、易错,且难以维护和升级。操作系统通过系统调用(System Calls) 这种机制,将这些复杂的底层细节封装起来,提供一套标准化的接口。开发者只需要调用 read()
, write()
, mkdir()
等简单的函数,操作系统内核就会完成所有底层的硬件交互。
核心思想 ✨
通过好的抽象(Abstraction)来隐藏细节。操作系统为应用程序构建了一个更优雅、更强大的虚拟机(Virtual Machine)模型,使得应用程序仿佛独占着整个计算机资源。
其层次关系**(如图 5 和图 6 所示)** 清晰地展示了操作系统如何作为中间层,连接应用与硬件。![[Pic/Pasted image 20250918110907.png]] [图 6:通过系统调用提供 API 的层次结构]
1.1.2 底层视角:作为资源管理器 💼
从系统本身的角度(“自下而上”)来看,操作系统的核心职责是资源管理器(Resource Manager)。
计算机的物理资源(如 CPU 时间、内存空间、磁盘空间、网络带宽等)是有限的,而多个应用程序和用户进程会同时对这些资源提出请求,这些请求之间可能存在冲突。
操作系统的关键作用在于:
- 资源共享:
- 时间共享:在时间维度上共享资源,如通过快速切换,让多个程序轮流使用单个 CPU,造成它们“同时”运行的假象。
- 空间共享:在空间维度上共享资源,如将磁盘和内存空间划分给不同的程序使用。
- 高效利用:通过调度算法和管理策略,提高系统资源的利用率和整体性能,同时最小化自身的开销。
- 保护与隔离:保护应用程序之间互不干扰,防止一个程序的错误导致整个系统崩溃。通过实施边界,确保程序的行为在其权限范围之内。
1.2 本课程的三大核心主题 🎯
【重点】 整个操作系统的学习将围绕以下三个核心概念展开:
虚拟化 (Virtualization)
- 定义:操作系统将物理资源(如 CPU、内存)抽象成易于使用的虚拟形式。例如,将一个物理 CPU 虚拟成无数个虚拟 CPU,让每个程序都以为自己独占 CPU;将物理内存虚拟成独立的虚拟地址空间。
- 核心问题:操作系统如何实现资源的虚拟化?这涉及到机制(Mechanism) 和策略(Policy)。机制提供实现某种功能的能力,而策略决定如何使用这些能力。
并发 (Concurrency)
- 定义:指在单个系统中同时处理多个任务或事件。这带来了许多挑战,如进程/线程间的同步、通信和死锁问题。
- 核心问题:如何编写正确的并发程序,以确保在共享数据时不会出现问题?
持久性 (Persistence)
- 定义:如何在系统断电后依然能保存数据。内存中的数据是易失的(Volatile),而我们需要将信息长久地存储在非易失性设备(如磁盘、SSD)上。
- 核心问题:如何通过文件系统(File System)来持久化地存储、组织和管理数据?这涉及到 I/O 系统调用。
第二章:核心机制:受限直接执行 ⚙️
本章我们将深入探讨第一个主题——虚拟化,并以 CPU 虚拟化 为例,揭示操作系统实现虚拟化的核心技术。
2.1 CPU 虚拟化的基本思想
【重点】 如何让多个程序看起来在“同时”运行在一个 CPU 上?
基本思路:分时共享 (Time Sharing) 💡
操作系统让一个程序在 CPU 上运行一小段时间,然后暂停它,切换到另一个程序运行一小段时间,如此循环往复。由于切换速度极快,从宏观上看,所有程序就像在同时运行。这就是 CPU 虚拟化的实现基础。
2.2 性能与控制的挑战
为了实现高性能,最直接的想法是让程序直接在 CPU 上执行。这被称为直接执行(Direct Execution)。但这种方式存在两个致命问题:
- 控制问题:如果程序直接在 CPU 上运行,操作系统如何确保该程序不会执行恶意或非法的操作(如访问不属于它的内存、独占 I/O 设备)?
- 调度问题:当一个程序正在运行时,作为软件的操作系统自身并没有在运行。那么,操作系统如何才能重新获得 CPU 的控制权,以便暂停当前程序并切换到另一个程序?
核心矛盾 💥
为了解决这个矛盾,现代操作系统采用了一种名为受限直接执行(Limited Direct Execution) 的技术。其核心思想是:大部分时间让程序直接运行,但在关键时刻(如需要执行特权操作或需要调度时),操作系统必须能够介入并重获控制权。
2.3 硬件支持:处理器模式 🛡️
【核心考点】 要实现“受限”,单靠软件是无法做到的,必须依赖硬件的支持。现代 CPU 提供了至少两种处理器模式(Processor Modes)。
CPU 内部通常有一个程序状态字(PSW, Program Status Word)寄存器,其中包含一个模式位(Mode Bit),用于标识当前的执行模式。
内核模式 (Kernel Mode)
- 模式位:设置为 1 (或特定值)。
- 权限:拥有最高权限,可以执行 CPU 指令集中的任何指令,包括访问所有内存和 I/O 设备。
- 使用者:操作系统内核在此模式下运行。
用户模式 (User Mode)
- 模式位:设置为 0。
- 权限:权限受限,只能执行一部分指令。对内存和 I/O 设备的访问受到严格限制。
- 使用者:所有应用程序都在此模式下运行。
2.3.1 特权指令 (Privileged Instructions)
【核心考点】 只能在内核模式下执行的指令被称为特权指令。如果用户模式下的程序试图执行特权指令,CPU 硬件会立即捕获这个行为,并触发一个异常(Exception),将控制权强制转交给操作系统内核来处理。
常见的特权指令示例:
- 所有 I/O 操作指令(如向磁盘发送读写命令)。
- 修改 CPU 模式位的指令(例如,将模式位从 0 设置为 1)。
- 修改内存管理寄存器的指令。
halt
指令。
实践出真知 💻
尝试在你的应用程序中嵌入一小段汇编代码,比如
in
或out
指令来直接访问硬件端口,或者hlt
指令。编译并运行它。你会发现程序会立即崩溃,并收到操作系统报告的“段错误(Segmentation Fault)”或“非法指令(Illegal Instruction)”错误。这就是处理器模式在起保护作用!
2.4 模式切换:系统调用 (System Call)
【核心考点】 既然用户程序不能直接执行特权指令,那么当它需要进行 I/O 操作(如读写文件)时该怎么办呢?答案是:请求操作系统内核代为执行。这个请求过程就是系统调用。
系统调用是用户程序进入内核的唯一、受控的入口。
2.4.1 trap
指令:从用户到内核
用户程序通过执行一条特殊的指令——trap
(也称为“陷阱”或“软件中断”)来发起系统调用。trap
指令会触发以下一系列由硬件自动完成的原子操作:
- 提升权限:将 CPU 的处理器模式从用户模式切换到内核模式。
- 保存上下文:将当前程序的关键寄存器状态(如程序计数器 PC、栈指针 SP、通用寄存器等)保存到内核指定的安全内存区域(通常是内核栈)。这是为了在系统调用结束后能够精确地返回到原来的位置继续执行。
- 跳转到处理程序:从一个预设的、由操作系统在启动时设置好的陷阱表(Trap Table) 或中断向量表(Interrupt Vector Table) 中,根据
trap
指令提供的编号,查找对应的内核处理程序的地址,并将 PC 设置为该地址,开始执行内核代码。
2.4.2 return-from-trap
:从内核返回
当操作系统内核完成了用户程序的请求后,它会执行一条对应的特权指令——return-from-trap
。这条指令同样由硬件执行,完成相反的操作:
- 恢复上下文:从内核栈中弹出之前保存的用户程序寄存器状态,恢复到 CPU 的各个寄存器中。
- 降低权限:将 CPU 模式从内核模式切回用户模式。
- 返回用户程序:PC 被恢复为用户程序调用
trap
后的下一条指令地址,用户程序继续执行,仿佛什么都没发生过。
实践出真知 💻
在 Linux 中,你可以使用
strace
命令来追踪一个程序执行期间的所有系统调用。例如,运行strace ls
,你将看到ls
命令为了列出文件,调用了openat
,read
,write
,close
等大量系统调用。每一行都代表了一次从用户模式到内核模式再返回的过程。
# 尝试运行这个命令,观察输出
<NolebasePageProperties />
strace echo "hello world"
你会看到类似 write(1, "hello world\n", 12)
的输出,这正是 echo
程序通过 write
系统调用请求内核将字符串输出到标准输出(文件描述符为 1)的过程。
2.4.3 trap
表:受控的入口点
【重点】 一个至关重要的问题是:trap
指令执行时,CPU 如何知道应该跳转到内核的哪个地址去执行代码?
答案是 trap
表。
- 什么是
trap
表:这是内存中一个由操作系统内核在启动时(在内核模式下)创建和设置的数据结构。它是一个函数指针数组,索引是系统调用编号,值是对应内核处理程序的内存地址。 - 为什么需要
trap
表:它确保了用户程序不能跳转到内核空间的任意位置。用户程序只能通过trap
指令,并提供一个合法的系统调用号,由硬件去查表并跳转。这保证了内核的入口是有限且受控的,极大地增强了系统的安全性。
其工作流程**(如图 4 所示)**,清晰地展示了用户态的 trap call
如何通过 Trap Table
定位到内核中正确的 Interrupt
处理程序。
![[Pic/Pasted image 20250918115721.png]] [图 4:trap 表(中断向量)的工作机制示例]
2.5 操作系统如何夺回控制权?
【核心考点】 我们已经解决了第一个问题(如何保护系统),但第二个问题依然存在:如果一个恶意或有 bug 的程序进入了无限循环,它从不进行系统调用,那么操作系统如何才能夺回 CPU 的控制权来进行进程调度呢?
2.5.1 协作式调度的问题
早期的操作系统(如早期的 MacOS 和 Windows)采用协作式多任务(Cooperative Multitasking)。这种方式依赖于应用程序“自愿”地调用一个特殊的系统调用(如 yield()
)来放弃 CPU。
这种方法的缺点显而易见:如果程序不合作,整个系统就会被“冻结”。
2.5.2 解决方案:定时器中断 (Timer Interrupt)
现代操作系统采用抢占式多任务(Preemptive Multitasking),其实现依赖于另一个关键的硬件支持:定时器硬件(Timer Hardware)。
- 设置定时器:在操作系统将 CPU 控制权交给一个用户程序之前,它会在内核模式下设置定时器硬件,指令其在未来一个短暂的时间点(例如 10 毫秒后)触发一个中断(Interrupt)。
- 程序运行:操作系统切换到用户模式,用户程序开始在 CPU 上运行。
- 中断触发:无论用户程序在做什么,当 10 毫秒时间到达时,定时器硬件会强制向 CPU 发送一个中断信号。
- 强制切换:CPU 接收到中断信号后,会立即暂停当前用户程序的执行,并自动执行与
trap
类似的过程:保存程序上下文,切换到内核模式,并根据中断号跳转到预设的内核中断处理程序。 - 内核决策:进入中断处理程序后,操作系统就重新获得了 CPU 的控制权。此时,它可以决定是让原程序继续运行,还是调度另一个程序运行。
2.6 中断、trap
与异常
【重点】 虽然它们的处理机制类似(都通过中断向量表跳转到内核处理程序),但它们的触发源不同:
- 中断 (Interrupt):由外部硬件设备异步触发。是异步的,因为它可能在任何指令执行期间发生。
- 示例:定时器中断、键盘输入中断、网络包到达中断。
trap
(陷阱):由用户程序在代码中主动执行特定指令(如系统调用指令)同步触发。- 异常 (Exception):由程序执行非法指令或遇到错误时同步触发。
- 示例:除以零、访问非法的内存地址、执行特权指令。
附录:一个操作系统的诞生 - SerenityOS 的故事 ✍️
【了解】 这部分内容来自开发者 Andreas Kling 的自述,它生动地展示了构建一个操作系统的激情、挑战与社区力量。
- 缘起:在 2018 年失业后,Andreas Kling 为了填补生活和寻找寄托,开始了一个编程项目。这个项目从一个 ELF 解析器、一个 Ext2 文件系统浏览器和一个 GUI 框架开始,最终演变成了一个完整的操作系统——SerenityOS。
- 命名:取名
SerenityOS
是为了纪念“宁静的祈祷”,希望这个项目能帮助他走在正确的道路上。 - 理念:项目的总体想法是构建一个他自己梦想中的日常使用系统,融合了 90 年代 GUI 的美学和现代 Unix 的直截了当的命令行。
- 成长:项目从零开始,完全自研,从内核到 Web 浏览器。Andreas 将开发过程录制并上传到 YouTube,他真实的分享吸引了越来越多的人关注。如今,SerenityOS 已经成长为一个充满活力的开源社区,拥有来自世界各地的数百名贡献者。
- 启示:这个故事告诉我们,操作系统开发不仅仅是冰冷的技术,它也可以是一个充满创造力、激情和人情味的过程。一个人的业余项目,在开放和分享的精神下,可以成长为一个影响成千上万人的伟大工程。