你现在能看这个视频,一是因为上帝至少给了你一只眼睛,二是因为操作系统决定让你看到它。如果你很有钱,你可能用的是 macOS;如果你对人生已经放弃,那你可能用着 Windows;如果你是那 3% 的硬核极客中的一员,那你可能在用 Linux。不管怎样,你的 CPU 正在运行大约其他 400 个程序,Chrome 无缘无故地吞噬着 20 GB 的内存,但不知为何,当你晃动鼠标时光标依然能移动。这并不正常。这是由有史以来最被低估的软件——操作系统,每秒执行数千次的神迹。第一个操作系统 GM-NAA I/O 于 1956 年在通用电气推出,因为一位工程师决定人类有更好的事情要做,而不是手动将打孔卡喂给一台两层楼高的 IBM 大型机。它一次只能运行一个程序,没有内存保护、用户或文件,但它崩溃的次数比 Windows Millennium Edition 还要少。

如今已是七十年后,在今天的视频中,你将最终理解引导程序处理、调度、线程、系统调用、虚拟内存、中断、特权环、I/O 和索引节点等如何完美协同工作。我们将通过弄清楚从你按下电源键到愤怒退出并关闭电脑的那一刻,计算机内部究竟发生了什么来实现这一目标。第一阶段:引导程序。你按下电源键,电流击中主板,CPU 以最原始的状态醒来。此时还没有内存管理,甚至没有文件的概念。它只是一个单核处理器,在固件中烧录的硬编码地址上执行指令。在现代机器上是 UEFI,而在古老的机器上则称为 BIOS。固件的工作是唤醒足够的硬件以找到磁盘,然后将其移交给引导程序。在 Linux 上是 GRUB(Grand Unified Bootloader),在 Mac 上称为 iBoot,而在 Windows 上则称为 Bootmgr。

但引导程序的工作很简单:在磁盘上找到内核并将其加载到 RAM 中。这就是交接点,此时 CPU 正在以完整的硬件特权运行内核代码。但你计算机的所有有趣之处,如文件、进程、窗口等,此时甚至还不存在。内核必须在接下来的几秒钟内从零开始构建这一切。但在我们继续之前,我们需要理解第二阶段:特权环。你的 CPU 受到多个特权级别的保护。在 x86 架构上有四个,但基本上只有两个重要:Ring 0(内核),它可以做任何事情;以及 Ring 3(用户空间),它可以运行应用程序,但需要为其他所有事物请求许可。目前,内核正在 Ring 0 中以 C 代码运行且没有任何护栏。一个错误的指针就可能导致整台机器“起火”。这就是为什么内核开发人员爱喝酒的原因。

但如果我们没有由 CPU 本身强制执行的这种隔离环,每个程序都能读取其他任何程序的内存并导致整个系统崩溃。但由于特权环的存在, buggy 的程序通常只能让自己崩溃。但现在我们进入第三阶段,内核在计算领域撒下了最大的谎言:虚拟内存。这个骗局的工作原理是这样的:当程序最终请求一个像这样的内存地址时,该地址并不存在。它是一个假的虚拟地址,由称为 MMU(内存管理单元)的硬件翻译成真实的物理地址。MMU 本身使用一个称为页表的数据结构,而内核目前正在构建这个页表。内存以被称为“页”的块形式分配,每页通常为 4 KB。

但真正有趣的是每个进程都有自己的页表,这意味着两个应用程序可以协同工作而不会互相干扰。例如,你的浏览器无法读取你密码管理器的内存,反之亦然。它们生活在只有内核才能看到的平行宇宙中。相当酷的是,MMU 还将最近的翻译缓存在一个称为 TLB(转换后备缓冲器)的小型结构中。一次翻译意味着将虚拟地址映射到物理地址。当程序访问不在 RAM 中的页面时,MMU 会引发页错误,这会唤醒内核并从磁盘加载一个页面,然后像什么都没发生过一样恢复程序。既然我们已经建立了这些内存谎言,现在是时候进入第四阶段:文件系统了。

你的磁盘在最低层面上是一长串数字块,但文件系统是对此撒谎的软件,向你展示这些漂亮的文件和文件夹。内核挂载了文件系统,但文件本身存储为称为索引节点(inode)的东西。但索引节点实际上并不是文件本身,而是包含元数据如大小、权限和时间戳,最重要的是指向磁盘上实际数据块的指针。但也要注意索引节点中不包含什么:文件名。你看,文件名存在于目录中,而目录本身只是将名称映射到索引节点号的特殊文件。这就是为什么你可以有多个文件名指向同一个文件的原因。

文件系统架构具有各种各样的个性,如 EXT4、NTFS 和 APFS(仅举几例),但现代文件系统的酷之处在于它们使用了一种称为日志记录的功能,该功能会在写入数据之前先写下你的意图。这意味着如果你在写入过程中意外拔掉电源线,大多数时候你最终得到的不会是一堆损坏的垃圾数据。但现在我们要进入第五阶段:设备驱动程序和中断。但首先,我们需要快速谈谈 Railway,它足够酷来赞助这期关于深奥操作系统知识的 11 分钟视频。Railway 是一个全栈智能云提供商,让你只需点击几下即可部署任何内容。

因此,你不必淹没在 YAML 中,而只需连接你的代码仓库,Railway 就会读取你的代码并自动为你设置正确的配置。或者,你可以使用新的 Railway CLI 及其官方代理技能,让 Code X、Vlad Code 或你最喜欢的 AI 代理助手来做所有脏活。开发人员喜欢 Railway 启动任何所需服务的简便性,以及它只为你实际使用的资源收费,而不是你预置的资源,这可以为云成本节省超过 65%。点击下方链接免费注册,升级时你将获得 20 美元的免费积分。抱歉打断一下,但现在我们要谈谈设备驱动程序和中断了。

此时我们有了内存和文件系统,因此内核开始加载设备驱动程序,这是将通用内核请求翻译成特定“象形文字”的专用代码。

给定芯片架构下的外部硬件,如显卡、Wi-Fi 卡和键盘等,都需要从磁盘加载驱动程序并注册到内核中。这些驱动实际上在内核模式下运行,这意味着一个有缺陷的驱动程序经常会导致整个操作系统崩溃,例如 Windows 蓝屏死机通常就源于显卡驱动。几年前,网络安全公司 CrowdStrike 发布的一个问题驱动程序几乎导致全球经济瘫痪。

一旦驱动程序加载完毕,内核就会启用一种称为中断的机制。你是否想过键盘是如何通知操作系统按键被按下的?操作系统并不会无限循环地询问,而是由键盘触发中断。这是一种电信号,会将 CPU 从当前执行的任务中拉出并跳转到内核中的中断处理程序。

中断是让计算机对输入做出即时反应的神奇机制,比如移动鼠标时触发中断使光标移动。如果 Wi-Fi 从互联网接收数据,也会触发中断唤醒网络栈供应用程序使用。基本上,整个机器都由硬件发出的微小电信号驱动,仿佛在说:“嘿,出事了,处理一下。”

随着所有驱动程序加载完毕,我们进入第六阶段:PID 1(第一个进程)。内核现在完全运行但很孤独,因此它创建了第一个用户空间程序 PID 1。在 Linux 上通常是 systemd,本质上进程就是正在运行的程序。

创建一个进程意味着内核需要分配内存、从磁盘加载可执行文件、设置虚拟地址空间和页表,并在名为进程表的巨大数据结构中添加条目。这是内核记录状态的方式,每个进程都有一个 PID(进程 ID)。

PID 1 很特殊,因为它是机器上所有其他进程的祖先。如果 PID 1 死亡,内核会恐慌并导致整个系统崩溃。但关键在于 PID 1 运行在环 3(用户空间),这意味着从此以后,机器上运行的所有程序都需要向内核请求许可。

这就引出了第七阶段:系统调用,这可能是你从未亲手编写过的计算领域最重要的 API。当进程想要读取文件时,不能直接访问磁盘,CPU 会物理拒绝。它必须发起系统调用,将参数放入特定寄存器并触发特殊指令,CPU 从而从环 3 切换回环 0。

这一边界是计算机安全的唯一原因。如果你曾用 C 编程,可能觉得自己是很硬核的低级工程师,但当你使用 print 函数时,并未意识到它底层实际上是在进行 write 系统调用。在 Linux 上约有 400 种不同的系统调用,它们是计算机真正的 API,其他一切只是构建其上的库。

两个最重要的系统调用是 fork 和 exec,用于在用户空间创建新进程。当你看到桌面时,已有数十甚至数百个运行中的进程,这便进入了第八阶段:调度器。但你的电脑只有八个 CPU 核心,如何同时管理数百个进程?

可以将调度器比作繁忙机场的空管。飞机是进程,调度员决定谁能在跑道(即 CPU)上着陆。内核开发者尝试过多种架构来解决这一挑战,但现代 Linux 使用最早可用虚拟截止时间优先技术。

这听起来像《艾尔登法环》里的 Boss,但它利用多种规则确保每个进程都能公平地获得 CPU 时间。然而我们面临另一个问题:某些应用希望在不增加多个进程开销的情况下同时执行多项任务,这便引入了第九阶段:线程。

线程共享相同的内存和文件描述符,但拥有不同的栈和程序计数器。突然之间,一个程序可以并行执行两项不同任务。但这就像把上了膛的枪指着脚。线程共享内存,同时写入同一变量常会导致竞态条件。

现代编程语言试图防止你“伤及自身”,如 Go 的协程或 Rust 借用检查器,后者会拒绝编译可能产生竞态条件的线程代码。虽然线程很棒,但两个完全不同的应用程序不能像那样共享内存。

当它们需要相互通信时该怎么办?这就来到了第十阶段:进程间通信(IPC)。想象我在 Linux 终端打开并用 cat 读取文件,得到输出后想在其中搜索特定术语。最简单的解决方案是使用管道,它发明于 1973 年且至今未被击败。

管道允许我们将 cat 和 grep(两个完全独立的进程)结合起来。操作系统创建一个管道,一个的输出成为另一个的输入。无需共享内存,只是字节流在两者之间流动,非常酷。还有其他 IPC 技术如套接字和消息队列,但主要思想是允许两个进程安全通信。

恭喜你,现在你已从底层理解操作系统的工作原理。但现在是时候“愤怒退出”了:让我们看看按下关机按钮时第十阶段会发生什么。PID 1 向每个进程发送 SIGTERM 信号,这是一种礼貌地要求它们停止工作的方法。

进程将保存状态并退出,但在超时后,会发送另一个称为 SIGKILL 的信号。这是游戏结束,文件系统将刷新日志并卸载,驱动程序释放硬件资源,内核同步内存到磁盘,中断被禁用。

最终 CPU 停止运行,固件切断电源,屏幕变黑。你终于自由了,可以出去接触一下现实世界(摸草)。感谢观看,我们下期再见。