try ai
科普
编辑
分享
反馈
  • 操作系统的核心原理

操作系统的核心原理

SciencePedia玻尔百科
核心要点
  • 操作系统通过创建进程隔离和私有虚拟内存等幻象,在有限的物理硬件上提供稳定、多任务的环境。
  • 有效的操作系统调度通过设定任务优先级并将 CPU 计算与缓慢的 I/O 操作重叠,来平衡吞吐量和延迟。
  • 锁和信号量等同步工具对于管理并发至关重要,但它们也引入了死锁和优先级反转等复杂风险。
  • 通过预写式日志(journaling)等机制,文件系统保证了数据的一致性和原子性,确保了即使在系统崩溃后数据的可靠性。

引言

操作系统是一位无形的魔术师,它将原始、混乱的硬件转变为一个连贯、可用的应用世界。它管理复杂性、提供私有资源的幻象并确保可靠性的能力,是所有现代计算的基础。然而,对许多人来说,这位魔术师的内部运作仍然是一个谜——一个极其复杂的黑匣子。本文旨在通过揭示使其成为可能的核心原理来填补这一空白。读者将踏上一段分为两部分的旅程。首先,在“原理与机制”中,我们将剖析其基础技巧,从创建进程和虚拟内存,到处理并发任务并确保数据在崩溃后幸存。然后,在“应用与跨学科联系”中,我们将看到这些原理如何应用于解决云计算、安全和分布式系统中的现实挑战。通过理解这些核心概念,我们可以从被动的用户转变为见多识广的观察者,从而欣赏那些为解决复杂计算问题而设计的优雅方案。

原理与机制

操作系统是你所能遇到的最伟大的魔术师。它接收计算机原始、混乱且有限的硬件——一块带有几个处理核心的硅片、一块内存和一些旋转的磁盘——然后变幻出一个充满幻象的世界。对于你运行的每一个程序,它都创造出一个拥有私有、强大计算机和广阔内存的幻象,仿佛一切都为其所用。它能同时处理几十个这样的虚幻机器,切换之平顺,让你感觉它们在同时运行。它以舞蹈大师般的灵巧协调它们之间的互动,防止它们陷入混乱。而最神奇的是,它能确保你宝贵的数据在突发、灾难性的断电后依然能够幸存。

本章将讲述这位魔术师如何表演他的戏法。我们将拉开帷幕,审视使现代操作系统成为可能的核心原理和机制。这是一个关于管理复杂性、平衡冲突目标以及在不可靠的具体硬件之上构建可靠的抽象世界的故事。

伟大的幻术师:进程及其堡垒

操作系统的基础戏法是​​进程​​。当你双击一个应用程序图标时,你不仅仅是在运行一个程序;你是在请求操作系统创建一个新的宇宙。进程是执行中的程序,但它远不止于此:它是一个环境,一个自成一体的世界,拥有自己的内存、自己的一组打开文件,以及对自己执行位置的认知。

这个宇宙的灵魂是隐藏在操作系统深处的一个数据结构,称为​​进程控制块 (PCB)​​。PCB 是内核关于进程的私有档案。它追踪一切:进程的ID、优先级、CPU 寄存器的内容、指向其内存空间的指针,以及其打开文件的列表。如果操作系统决定增加新功能,比如允许进程为自己打上元数据标签以进行调试或资源跟踪,这些信息也会存放在 PCB 中。但是,由于 PCB 是进程存在的万能钥匙,它必须受到偏执狂般的严密保护。允许一个进程直接涂写自己的 PCB——或者更糟,涂写别人的 PCB——就像让小说中的一个角色重写情节一样。那将是彻头彻尾的混乱。

这就引出了​​隔离​​原则。保护内核并将一个进程与另一个进程隔离开来的堡垒之墙不是由石头构成,而是由硅构成。CPU 至少有两种操作模式:拥有特权的​​监管模式​​(或内核模式)和受限的​​用户模式​​。操作系统内核在监管模式下运行,拥有对所有硬件的神级访问权限。所有的用户程序——你的网页浏览器、文本编辑器、游戏——都在用户模式下运行,其权力受到严格限制。任何用户模式程序试图执行特权指令(如停止机器或直接操作设备)的尝试,都会导致硬件立即将控制权捕获回操作系统,操作系统通常会终止违规的进程。

这种分离不仅仅是一个好主意;它是稳定性的基石。考虑一下栈,即程序用来跟踪函数调用的内存区域。一个进程实际上有两个栈:一个用于其自身代码的用户栈,和一个独立的、受保护的内核栈,用于它请求操作系统为其执行操作时使用。如果操作系统在为用户进程处理信号时,试图“高效地”在内核栈上运行用户的信号处理代码,会发生什么?一旦用户代码试图访问其栈,CPU 的内存保护单元就会发出警报:一条用户模式指令正试图触碰一个仅限监管模式访问的内存页!将会发生一个故障,这个方案就会失败。这种由硬件强制执行的严格分离,确保了即使是一个有 bug 或恶意的用户程序也无法破坏内核的内部状态,这一原则对于一个安全的多任务系统来说是绝对必要的。

无限、私有内存的幻象

每个进程都生活在一个拥有自己私有内存的宇宙中,这是一个从零延伸到数十亿字节的广阔、线性的地址空间。但当然,你计算机中的物理内存 (RAM) 是一个有限的、共享的资源。这种私有、广阔内存的宏大幻象被称为​​虚拟内存​​,它是操作系统最巧妙的创造之一。

历史上,创造这种幻象的一种方法是通过​​分段​​。其思想是将一个进程的地址空间划分为逻辑段——一个用于代码,一个用于数据,一个用于栈,等等。每个段都有一个基地址(它在物理内存中的起始位置)和一个限制(它的大小)。当两个进程运行同一个程序时,操作系统可以施展一个聪明的技巧:它可以将它们的代码段都映射到相同的物理内存,但将其标记为只读。同时,每个进程为其数据段获得自己私有的、可写的物理内存。这节省了大量的 RAM。硬件的内存管理单元 (MMU) 会检查每一次内存访问,确保进程不会写入只读段或访问超出其段限制的内存。为了管理这种共享,操作系统对共享的代码段维护一个引用计数,只有当最后一个使用它的进程退出时才释放它。

在现代系统中更主流的方法是​​分页​​。分页不是使用可变大小的逻辑段,而是将虚拟内存和物理内存都划分为小的、固定大小的块——通常是 4 或 8 千字节——分别称为​​页​​和​​页帧​​。操作系统为每个进程维护一个​​页表​​,它就像一张地图,将进程请求的每个虚拟页转换到 RAM 中的一个物理页帧。

但正是在这里,操作系统的优美、递归的特性显露出来。页表本身存放在哪里?它是一个数据结构,所以它也必须存储在内存中!让我们考虑一个标准的 32 位系统,其中地址是 32 位长,页面大小是 4 KiB (2122^{12}212 字节)。地址的高 20 位标识虚拟页号,低 12 位是该页内的偏移量。由于页号有 20 位,因此有 2202^{20}220(约一百万)个可能的虚拟页。如果每个将虚拟页映射到物理帧的页表项 (PTE) 占用 4 字节,那么单个进程的页表就需要 220×4 bytes=4 megabytes2^{20} \times 4 \text{ bytes} = 4 \text{ megabytes}220×4 bytes=4 megabytes 的内存!这整个 4MB 的表必须存放在某个地方。解决方案是什么?操作系统将页表本身也分成页,并使用一个二级页表来映射它们。这就像“乌龟背乌龟,一直背下去”,一个优雅的、自举的解决方案来解决管理内存映射的问题。

杂耍的艺术:并发、并行与调度

当进程生活在它们私有的、受保护的世界中时,操作系统必须赋予它们生命,通过杂耍般的操作创造出同时执行的幻象。这就引出了​​并发​​和​​并行​​之间的微妙区别。并发是一种构建程序以同时处理多个任务的方式。并行是关于利用多个 CPU 核心物理上同时做多个任务。你可以在单个核心上实现并发(通过在任务之间快速切换),但你需要多个核心才能实现并行。

然而,拥有多个核心并不能自动让你的程序变快。想象一个软件团队在一台双核机器上工作。他们重写了应用程序以使用许多线程,希望能将其速度提高一倍。但令他们失望的是,运行时间几乎没有变化。罪魁祸首是一个隐藏的瓶颈。他们的任务包含一个快速、可并行的计算步骤(CCC)和一个缓慢的日志记录步骤(LLL),后者由一个单一的全局锁保护。由于日志记录比计算慢得多(C≪LC \ll LC≪L),线程们大部分时间都在为这个锁排成单行等待。串行的日志记录部分主导了总时间,使得额外的核心毫无用处。这是​​阿姆达尔定律​​的一个经典展示:你所能获得的最大加速受限于任务中无法并行的那一部分。解决方法是什么?给每个任务自己的锁,打破瓶颈,允许两个日志流并行进行,最终实现期望的加速。

这个杂耍动作由​​调度器​​执行,它必须不断地做出艰难的决定。其主要冲突是在最大化​​吞吐量​​(单位时间内完成的总工作量)和最小化​​延迟​​(交互式任务响应的延迟)之间。想象一下,混合着长时间运行、消耗大量 CPU 的计算密集型作业和短时间、交互式的 I/O 密集型作业(比如等待按键的文本编辑器)。一个天真的调度器可能会让一个计算密集型作业长时间运行,以避免切换的开销。但这会造成可怕的“护航效应”:所有短的、交互式的作业都被卡住等待。用户看到的是一个冻结的屏幕,而磁盘驱动器则闲置着,等待一个永远不会到来的命令。

一个更聪明的​​抢占式​​调度器则反其道而行之。它给予 I/O 密集型任务高优先级。它让它们运行一个非常短的突发时间——刚好足够完成它们的工作并发出一个 I/O 请求(例如,从磁盘读取文件)。然后,当缓慢的磁盘正忙时,调度器可以切换回那个长的计算密集型作业。这种​​CPU-I/O 重叠​​是使系统感觉既快又高效的秘诀。

但即使是最复杂的优先级方案也有陷阱。考虑一下臭名昭著的​​优先级反转​​问题。想象一下,CPU 1 上的一个高优先级任务需要一个被 CPU 2 上的一个低优先级任务锁定的资源。这个高优先级任务必须等待。现在,一大批中等优先级的任务到达 CPU 2。根据抢占式调度的规则,它们都会在那个低优先级任务再次获得机会之前运行。结果是灾难性的:高优先级任务现在实际上被所有较低优先级的任务阻塞了,完全颠覆了调度策略。这不仅仅是一个理论问题;它曾导致火星探路者(Mars Pathfinder)探测器发生系统重置,直到地球上的工程师诊断出问题并上传了一个补丁。

伟大的协调者:同步与死锁的危险

当并发进程必须合作或共享资源时,它们需要行为准则。这就是​​同步​​的领域。我们已经看到了一个使用不当的锁如何扼杀并行性;现在让我们看看同步工具如何被用于好的方面。

考虑一个物联网 (IoT) 传感器中心,它运行在一个微小的、电池供电的微控制器上。一个消费者任务等待来自传感器的一批数据。它可以​​忙等待​​,在一个紧密的循环中检查一个标志,燃烧 CPU 周期和宝贵的电池寿命。一个更优雅得多的解决方案是使用像​​信号量​​这样的同步原语。消费者任务对信号量执行 wait 操作,操作系统将其置于休眠状态。在这种休眠状态下,它几乎不消耗任何电力。当生产者任务准备好数据时,它对信号量执行 signal 操作,唤醒消费者。这种阻塞式同步方法带来的节能效果是惊人的——通常超过 98%——使得电池供电的设备可以运行数月或数年,而不是数小时。

然而,这个由锁和信号量组成的世界充满了危险。最阴险的是​​死锁​​,这是一种致命的拥抱,其中两个或多个进程陷入循环等待,每个进程都持有着对方需要的资源。死锁的发生必须同时满足四个条件:互斥、持有并等待、不可抢占和循环等待。只要打破其中一个条件就足以防止死锁。

想象一个网络服务的设计会议,其中线程需要获取一些数据包缓冲区,然后获取一个共享表的锁。一个提议的策略是,在尝试获取锁之前,先获取线程可能需要的所有缓冲区。这个策略很有趣。一个线程可能会在等待锁的同时持有缓冲区,所以“持有并等待”的条件仍然满足。然而,它建立了一个严格的​​资源排序​​:缓冲区总是在锁之前被获取。线程永远不会在等待缓冲区时持有锁。这种严格的排序使得循环等待变得不可能,从而防止了死锁。但这种安全性是有代价的。线程可能会在等待锁的同时,预先分配并长时间持有许多缓冲区,即使它们最终只使用了少数几个。这可能会降低整体内存可用性并损害性能。这是另一个经典的操作系统权衡的例子:安全性与活性和效率。

持久的记录者:在崩溃中幸存并与世界对话

我们操作系统魔术师的最后一个角色是扮演一个记录者,管理持久的存储世界以及与外部设备的通信。这个 I/O 的世界是缓慢和异步的。当磁盘终于准备好你请求的数据时,它不是寄一封信;它是通过硬件​​中断​​来轻拍 CPU 的肩膀。

处理这些中断需要一种微妙的平衡。操作系统必须立即响应,但它不能在可能禁用其他中断的特殊中断上下文中花费太多时间。解决方案是一个优美的分层设计。即时响应是一个闪电般的​​上半部​​处理程序。它只做最少的工作——确认设备,也许复制少量数据——然后安排其余的工作稍后完成。这个延迟的工作在​​下半部​​(或 softirq)上下文中运行,此时中断已再次启用,从而保持系统的响应性。对于可能需要休眠的更长任务(例如,为了获取一个锁),工作被移交给一个通用的​​工作队列​​。这种优雅的层次结构使得操作系统既能紧急响应又能保持高效。

对记录者最终的考验是确保写下的东西能在灾难(如突然断电)中幸存下来。当你的电脑崩溃并重启时,你期望你的文件处于一致的状态。这是​​文件系统​​的保证。如果你只是对一个文件执行 write(),操作系统可能会为了效率将数据缓存在易失性的 RAM 中;一次崩溃意味着数据丢失。但如果你调用 [fsync](/sciencepedia/feynman/keyword/fsync)(),你是在给操作系统一个直接的命令:“在这些数据安全地存到物理磁盘之前不要返回” [@problem-id:3664582]。

对于更复杂的操作,比如重命名一个文件,情况如何?这必须是​​原子​​的。你绝不应该在重启后发现新旧文件名都存在,或者两者都不存在。为了提供这种保证,现代文件系统使用​​日志记录​​(journaling)或​​预写式日志​​(write-ahead logging)等技术。在修改磁盘结构之前,文件系统首先在一个特殊的日志(或 journal)中写下一条笔记,描述它将要做什么。只有在日志条目安全地存到磁盘上之后,它才执行实际的操作。如果中途断电,重启时操作系统只需读取日志。然后它可以使用日志条目安全地完成未完成的操作或将其回滚,从而保证文件系统结构永远不会处于损坏、不一致的状态。这也许是操作系统最令人印象深刻的幻象:从物理世界的混乱中创造出秩序和永恒。

应用与跨学科联系

在我们迄今的旅程中,我们探索了操作系统的基本原理——那些赋予沉寂机器生命的优雅规则和巧妙机制。但这些原理不仅仅是计算机科学家教科书中的抽象奇珍。它们是编织我们现代数字世界结构的无形但不可或缺的线索。要真正欣赏它们的美丽和力量,我们必须看到它们在行动中,解决实际问题,防止混乱,并促成那些曾是科幻小说素材的技术。现在,让我们走出内核的核心,进入熙熙攘攘的应用世界,去见证操作系统扮演一丝不苟的会计、坚定不移的守护者和聪明的策略家。

操作系统:一丝不苟的会计

从本质上讲,操作系统是稀缺资源的管理者。就像一位杰出的会计师,它必须追踪每一微秒的 CPU 时间和每一个字节的内存,以确保公平和效率。这项任务始于 CPU 调度。仅仅给每个进程一个“轮次”是不够的;我们需要理解和预测性能。

想象一个简单的场景:一个父进程启动,并在灵光一闪间,创建了 nnn 个子进程,它们几乎在同一瞬间都准备好运行。如果操作系统使用简单的轮询(Round Robin)调度器,即给每个进程一个固定的时间片 qqq,那么最后一个子进程 CnC_nCn​ 何时才能首次运行?通过第一性原理推理,我们可以追溯事件。父进程运行它的时间片,然后 n−1n-1n−1 个兄弟进程各自获得它们的轮次。每次 CPU 从一个进程切换到另一个进程时,都会产生一个虽小但非零的开销 hhh。可怜的 CnC_nCn​ 的总等待时间是所有这些时间片和所有这些开销的总和。它的响应时间不是某个随机、不可预测的量;它是一个确定性函数,可以计算为 n(q+h)−an(q+h) - an(q+h)−a,其中 aaa 是它精确的到达时间。这个简单的思想实验揭示了一个深刻的真理:在一个设计良好的系统中,性能不是魔法;它是其底层算法的可预测结果。

现在,让我们将这个想法扩展到庞大的云计算世界。一台物理服务器可能为不同的客户托管成百上千个应用程序,每个都运行在“容器”内。在这里,简单的轮流制是不够的。我们需要严格的保证。如果一个客户的应用程序突然变得非常繁忙,绝不能允许它窃取其他人的 CPU 时间。这就是操作系统的会计变得真正复杂的地方,它使用像 Linux 的控制组 (cgroups) 这样的机制。你可以把它想象成给每组进程一个严格的预算:在一个给定的“周期”内可以使用的 CPU 时间“配额”。如果它超出了预算,它就会被“节流”——被礼貌地要求等到下一个周期开始。操作系统会保留一个详细的分类账,可以在像 cpu.stat 这样的文件中看到,它一丝不苟地记录了使用了多少 CPU 时间,经过了多少个周期,以及有多少时间花在了被节流上。通过分析这些数据,系统管理员可以精确地测量一个应用程序的有效 CPU 利用率,并验证资源限制是否被强制执行。这是使得多租户云服务和像 Kubernetes 这样的容器编排平台成为可能的基本会计技巧。

操作系统的会计职责同样重要地延伸到内存。当内存已满并且需要一个新页时,应该驱逐哪个现有的页?一个看似公平和简单的策略是先进先出 (FIFO):驱逐在内存中停留时间最长的页。这能有什么问题呢?让我们考虑一个正在执行事务的数据库。该事务可能会多次访问数据页 aaa 和 bbb,然后在提交之前,它需要写入其重做日志页 ℓ\ellℓ。如果该事务的访问模式导致 ℓ\ellℓ、aaa 和 bbb 先被加载,然后需要一个新页 ccc,FIFO 将尽职地驱逐最旧的页:ℓ\ellℓ。片刻之后,当事务试图通过访问 ℓ\ellℓ 来提交时,它发现该页不见了!这会触发一次昂贵的页错误,以从磁盘重新读取日志。这不是一个罕见的偶然事件;它是一个简单的算法未能理解真实世界应用程序访问模式而导致的病态后果。这是一个绝佳的教训:在系统设计中,最明显或“公平”的解决方案可能是微妙的,有时甚至是灾难性地错误。艺术在于设计与它们所服务的应用程序和谐共存的算法。

操作系统:坚定不移的守护者

除了管理资源,操作系统还必须是一个守护者,执行规则以防止混乱,并保护系统免受意外和恶意行为的侵害。

这个角色在并发世界中最为明显。想象一个简单的日志文件,同时被多个线程写入。如果两个线程试图同时追加它们的消息,结果可能是一团乱码。一个线程可能找到了文件的末尾,但在它写入之前,另一个线程写入了它的消息。然后第一个线程覆盖了第二个线程。这是一个经典的“竞争条件”。为了防止这种无政府状态,操作系统提供了秩序的工具。其中最优雅的一个是 O_APPEND 标志。当一个文件以这个标志打开时,操作系统保证每一次 write 操作都是原子的:内核本身将找到文件的末尾,并将数据作为一个单一的、不可分割的步骤写入。这就像告诉操作系统,“就把这个放在最末尾;我相信你会处理好细节。”对于更复杂的操作,操作系统提供锁,允许程序员构建一个“临界区”——一段一次只能有一个线程进入的代码区域,将像“寻找到末尾”和“写入”这样的非原子操作序列转变为一个单一的、逻辑上的原子单元。

有时,无序的状态更为微妙和终结。如果进程 AAA 正在等待进程 BBB 持有的资源,而进程 BBB 反过来又在等待进程 AAA 持有的资源,它们将永远在致命的拥抱中等待。这就是死锁。它不仅仅是一个理论概念;它出现在真实、复杂的系统中。考虑一个嵌入式设备,其中一个 CPU 线程启动一次直接内存访问 (DMA) 传输并等待完成信号,但 DMA 引擎本身需要获取等待中的 CPU 线程持有的锁才能写回其状态。这就产生了一个循环依赖:CPU 等待 DMA,而 DMA 等待 CPU。通过使用操作系统理论提供的形式化工具——等待图(wait-for graph)——来为系统建模,我们可以将这些依赖关系可视化为进程之间的有向边。该图中的一个环,T1→D→T1T_1 \to D \to T_1T1​→D→T1​,立即揭示了死锁,将一个神秘的系统冻结转变为一个可诊断和可解决的问题。

守护者的终极职责是安全。在我们这个相互连接的世界里,我们经常需要在同一台物理机器上运行来自不同、不受信任来源的代码。操作系统必须建立墙壁来将它们分开。但并非所有的墙都是一样的。让我们比较两种主流的隔离技术:容器和虚拟机 (VM)。乍一看,它们似乎很相似,但它们的安全模型却大相径庭。容器本质上是一个沙盒化的进程,它与宿主机的操作系统内核共享。如果一个攻击者在容器内发现漏洞并获得了内核级权限,他实际上已经攻陷了宿主内核。这就像一个小偷拿到了一把可以打开大楼里所有公寓的万能钥匙。相比之下,虚拟机运行着自己完整的、独立的客户操作系统,有自己的客户内核。虚拟机内的内核漏洞只会危及该客户机。这就像闯入了一间单独的公寓。为了逃逸并攻击宿主机,攻击者必须找到并利用虚拟机监控程序(hypervisor)——即为虚拟机模拟硬件的软件——中的第二个、独立的漏洞。这类似于撬开公寓的主门锁。这个简单的类比,植根于共享内核与独立内核的核心架构差异,解释了安全态势的深刻不同,以及为什么虚拟机被认为是更强的隔离边界。

这引导我们走向一个美丽的概念融合,即操作系统将其作为可靠性守护者的角色与其作为安全守护者的角色结合起来。我们如何构建一个既能抵抗意外断电又能抵御恶意攻击者的存储系统?我们可以将操作系统的预写式日志技术与密码学原理融合起来。一个日志或预写式日志确保了原子性:一批更新在崩溃后要么完全完成,要么根本不完成。我们可以通过向每个日志条目添加一个用秘密密钥计算的消息认证码 (MAC) 来加强这一点。通过“链接”这些 MAC——使记录 iii 的 MAC 依赖于记录 i−1i-1i−1 的 MAC——我们创建了一个不可破解的密码链。没有秘密密钥的攻击者无法伪造一个有效的日志条目或重新排序现有的条目而不被发现。这是一个跨学科设计的完美例子,其中操作系统原理和密码学联手创造出比任一领域单独所能产生的系统都要强大得多的系统。

操作系统:聪明的策略家

操作系统处于一个独特而富有挑战性的位置:它是一位策略家,不断地在物理硬件的混乱世界和软件的清晰抽象世界之间进行调解。它还在其通用的服务与高性能应用程序的专业需求之间进行协商。

在面对现代异构硬件时,这种策略至关重要。如今许多处理器都是非对称的,混合了高性能的“大”核和节能的“小”核。如果我们有一个任务要运行,比如说一个网卡的设备驱动程序,操作系统应该选择哪个核心?答案并非显而易见。这是一个权衡。人们可能认为“大”核总是更好,但如果任务主要由使用 DMA 的大数据传输主导,那么连接到核心的有效内存带宽可能是决定性因素,而不是其原始计算速度。通过创建一个简单的性能模型,我们可以推导出数据传输大小的盈亏平衡点 S⋆S^{\star}S⋆,低于此点一个核心更好,高于此点另一个核心胜出。这表明操作系统不仅仅是一个简单的任务分派器,而是一个理解硬件拓扑以优化性能和功耗的智能战略家。

操作系统的策略技巧在其与复杂应用程序的关系中也受到考验。考虑一个高性能数据库。为了快速运行,它在“缓冲池”中管理自己的数据页缓存。但是操作系统,为了提供帮助,也在其“页缓存”中缓存文件数据。当数据库读取一个文件时,数据首先被加载到操作系统的页缓存中,然后复制到数据库的缓冲池中。结果是“双重缓存”,这是一种消耗宝贵内存的浪费性重复。一个优秀的操作系统认识到它并不总是最了解情况。它充当一个灵活的框架,为这些专家级应用程序提供特殊工具。它提供直接 I/O ([O_DIRECT](/sciencepedia/feynman/keyword/o_direct)),这是一个允许数据库说“谢谢,但我会自己管理缓存;请将数据直接传输到我的缓冲区”的接口。它还提供像 madvise 和 posix_fadvise 这样的建议性接口,让应用程序可以给出提示,比如“我用完这块数据了,你可以随意回收它的内存”。这允许一种合作关系,消除了冗余并最大化了性能。

这种提供明确定义接口的主题延伸到进程如何通信。在单台机器上,我们可以使用 POSIX 管道连接一个生产者和一个消费者进程。这与使用 TCP 网络连接与 localhost 通信相比如何?。在这里,类比思维非常强大。管道和 TCP 流都像字节的管道。为不可靠的互联网设计的 TCP 流,具有用于重传和拥塞控制的复杂机制。管道是本地的、更简单的,但它仍然有令人惊讶的复杂特性。如果消费者停止读取,管道的内部缓冲区会填满,操作系统会自动暂停生产者。这是一种自然的“背压”机制,在精神上类似于 TCP 的流控制窗口。此外,对于小于 PIPE_BUF 的小写入,操作系统保证原子性,确保消息不会混淆。通过比较这些机制,我们学会像系统设计师一样思考,不仅问“我如何发送数据?”而且问“我需要什么样的可靠性、顺序和流控制保证?”

当我们模糊机器之间的界限时,策略家的角色变得最具挑战性。当为了备份而对一个正在运行的虚拟机进行快照时会发生什么?[@problem-id:3689871]。如果虚拟机监控程序只是对虚拟磁盘的块拍一张即时照片,那么最终的状态是“崩溃一致性”的。对于虚拟机内部运行的数据库来说,这就像是突然断电了。它将使用自己的恢复日志回到一致的状态,但快照本身并不“干净”。为了实现一个原始的、“应用一致性”的状态,需要一场优美的、多层次的策略舞蹈。虚拟机监控程序必须与客户操作系统协调,后者又必须与数据库应用程序协调,告诉它在拍摄快照之前刷新所有缓存并在一个已知的良好点暂停。这说明了在多个抽象层之间维持一致性所需的精妙协商。

也许操作系统必须执行的最深刻的调解是关于时间本身的概念。什么是“时间”?这似乎很简单,直到你将一个正在运行的虚拟机从一台物理主机迁移到另一台。新主机的物理时钟晶体振荡频率可能略有不同。如果天真地读取挂钟时间,它甚至可能看起来会向后跳。一个设计良好的操作系统为这种时间冲击做好了准备。它提供至少两种时钟:一个 CLOCK_REALTIME,用于跟踪民用时间;和一个 CLOCK_MONOTONIC,它庄严地承诺永不后退。如果挂钟时间倒退,单调时钟会保持稳定。然后,操作系统会温和地“校正”实时时钟,通过巧妙地调整其频率来缓慢地缩小差距,而不是做出可能混淆应用程序的突兀跳跃。但即使这样也不足以在分布式系统中正确地排序事件,因为不同机器上的单调时钟并不同步。为了解决这个问题,我们必须转向一种完全不同的时间:逻辑时间。例如,兰伯特时钟 (Lamport clock) 根本不是一个时钟,而是一个简单的计数器,它随每个事件递增并在消息中交换。它遵循一套简单的规则,保证如果事件 AAA 导致了事件 BBB,那么 AAA 的逻辑时间将总是小于 BBB 的逻辑时间。这是操作系统最后的策略杰作:调和现实世界时钟的混乱物理学与分布式算法的严格逻辑要求,确保在其管理的世界中,因果关系总是被保留。