try ai
科普
编辑
分享
反馈
  • 操作系统的演进

操作系统的演进

SciencePedia玻尔百科
核心要点
  • 操作系统通过创建强大的抽象(如虚拟文件系统VFS)来演进,以便在复杂多样的硬件之上呈现一个简单、统一的接口。
  • 安全性已从简单的访问控制发展到复杂的、基于硬件的信任机制(如安全引导)以及概率性防御措施(如地址空间布局随机化ASLR)。
  • 操作系统设计是一个持续的平衡过程,需要在各种基本权衡中导航,例如隔离与性能(进程与线程)以及一致性与速度(日志模式)。
  • 多核处理器的兴起带来了复杂的并发挑战,推动了规范的同步模式的发展,以防止死锁和竞争条件。
  • 现代系统正朝着操作系统与应用程序之间的共生关系发展,从而实现性能和资源管理的协作优化。

引言

操作系统的演进是计算机科学中的一个基础性故事,详细描述了从原始硬件到我们日常使用的复杂数字环境的历程。这是一个驯服复杂性、建立秩序并创造强大幻象的叙事,这些幻象支撑着从移动应用到全球云服务的一切。其核心在于,操作系统必须解决管理有限、复杂的硬件资源的巨大挑战,同时为无数应用程序提供一个稳定、安全和高效的平台。本文探讨了为应对这一挑战而生的各项原则,是如何随着新技术的出现和新威胁的产生而随时间发展的。

读者将踏上这段演进之旅。在“原理与机制”部分,我们将剖析操作系统的核心构建模块,从引导过程的最初火花和抽象的创建,到错综复杂的并发之舞和坚固的安全堡垒。然后,我们将在“应用与跨学科联系”部分探讨这些基本原理如何在现实世界的应用中体现,如何塑造软件架构,以及如何与它们所承载的程序建立不可破坏的契约。这次探索揭示了操作系统不仅仅是一个软件,更是一个活生生的思想体系,不断适应新的挑战并塑造着计算的未来。

原理与机制

想象一下,你被交予一个全新宇宙的钥匙。这是一个由纯粹逻辑、硅和电构成的宇宙,但此刻,它黑暗、寂静、无形。你的任务是赋予它生命——建立物理法则,创造其中的居民,并给予他们互动、成长和保护自己免受混乱侵害的方式。这便是设计操作系统的宏伟挑战。操作系统的演进是一个发现的故事,一段从粗糙、简单的规则到支配我们数字生活的惊人复杂而优雅的结构的旅程。这是一个驯服原始硬件、构建优美抽象,并不断与性能和安全这对孪生恶魔作斗争的故事。

创世的火花:引导与掌控

在操作系统能够进行管理之前,它必须首先存在。它必须依靠自身的力量“自举”启动。当计算机通电时,处理器一无所知。它遵循一个单一、简单的指令:前往一个固定的地址,并开始执行在那里找到的任何东西。这段初始代码,即引导加载程序(bootloader),肩负着为机器注入生命的艰巨任务。

它的首要工作就像一位绘制未知大陆的先驱。引导加载程序必须弄清楚物理内存的布局,而这片“大陆”很少是简单平坦的平原。它通常是一片破碎的领域,充满了被底层硬件占用的空洞和保留区域。利用系统固件提供的简陋地图(如个人电脑上古老的e820映射表),引导加载程序必须扫描一块连续、可用的土地,其大小足以容纳内核——操作系统的核心。这是一场在受限条件下寻找安全避风港的搜索,一场与硬件进行微妙的协商,以找到一块可寻址、对齐的内存来开始构建它的世界。

但在一个充满敌手的世界里,仅仅加载内核是不够的。你如何知道你正在加载的内核是你信任的那个?从简单的加载到安全引导的演进,标志着从天真信任到严格“信任仪式”的深刻转变。现代系统不仅仅是加载下一阶段;它们会对其进行“度量”,创建一个数字指纹(一个加密哈希)。这个指纹随后被提交给一个硬件信任根,如​​可信平台模块(TPM)​​,它扮演着一个廉正的仲裁者角色。TPM可以持有秘密,这些秘密只有在指纹与已知的、良好的值匹配时才会被“解封”。

为了防止攻击者简单地重放一个旧的、有漏洞但经过签名的内核(一种回滚攻击),这个过程被设计成动态的。每次启动时,TPM都会向该过程中注入一个随机的​​nonce​​(一次性随机数)。想要回滚系统的攻击者不仅需要有一个旧的、签名的镜像,还必须正确猜出这个短暂的nonce。这个nonce的不可预测性是该防御的基石。我们可以用​​最小熵​​(min-entropy)的概念来量化这种不可预测性,它衡量了猜出最可能结果的难度。即使随机数生成器有轻微的偏差(例如,一个比特为‘1’的概率是 p=0.55p=0.55p=0.55 而不是完美的 0.50.50.5),对于一个 k=128k=128k=128 比特的nonce,其最小熵 HTPM=−klog⁡2(p)H_{\mathrm{TPM}} = -k \log_{2}(p)HTPM​=−klog2​(p) 也是一个天文数字。攻击者成功的概率,即使尝试数千次重启,也会变得微乎其微,数量级在 10−2910^{-29}10−29 左右——这证明了在引导链中注入经过计算的不可预测性的强大威力。这个信任链从一个不可变的硬件信任根开始,贯穿引导过程的每一个阶段,确保了操作系统在可验证的完整性基础上开始其生命周期。

构建一个抽象的世界

一旦内核运行起来,其主要目的就是从混乱中创造秩序。它通过创建强大的​​抽象​​来实现这一点。抽象将复杂、混乱、特定于硬件的现实,呈现为一个干净、简单、统一的接口给应用程序员。这也许是操作系统设计中最优美和最核心的原则。

思考一下不起眼的文件。对一个程序来说,文件是一个简单的字节序列。你通过名称打开它,从中读取,向其写入,然后关闭它。然而,在这平静的表面之下,隐藏着各种令人困惑的磁盘上的现实。一个文件系统可能使用​​inode​​(索引节点)来组织数据,这是一种复杂的数据结构,包含元数据和指向实际数据块的指针,并使用高效的B树索引在大型目录中查找文件。而另一个,比如经典的文件分配表(FAT)系统,可能只使用简单的目录条目,指向一个散布在磁盘上的链表中的第一个数据簇。

操作系统如何从这两个截然不同的世界中呈现出一个简单的“文件”概念呢?它使用了一个抽象层,即​​虚拟文件系统(VFS)​​。当一个程序想要打开一个文件时,它与VFS对话。VFS再与底层硬件的特定驱动程序对话。对于基于inode的系统,VFS可能会找到一个丰富的、存在于磁盘上的inode来使用。对于FAT系统,由于不存在这样的结构,VFS驱动程序将动态地在内存中合成一个inode,用目录条目和挂载时的默认值来填充它。VFS创建了一个文件的“柏拉图式理想”——一个内存中的inode和一个目录项(​​dentry​​)——所有程序都可以与之交互,而无需关心磁盘格式的丑陋细节。这种优雅的设计允许你插入U盘、网络共享和固态硬盘,它们各自有自己的内部逻辑,但你看到它们都是一个单一、统一的文件系统的一部分。

另一个基本的抽象是​​进程​​的概念:一个执行中的程序。操作系统必须同时管理许多进程,给予每个进程它独占整个机器的幻觉。实现这种幻觉的主要工具是硬件的内存保护,它在进程地址空间之间建立“墙壁”。这创造了强大的​​隔离​​:一个进程中的崩溃或错误通常不会伤害到另一个进程。然而,这些墙是厚重的。进程之间的通信是一个刻意的、相对缓慢的行为,需要由内核进行中介。

如果任务需要紧密合作并快速通信怎么办?为此,操作系统提供了​​线程​​。线程就像住在同一所房子(进程地址空间)里的多个工人。它们共享内存,使得通信异常迅速,但这也有代价。它们之间没有墙壁。一个错误的线程就可能破坏共享状态,导致整个进程崩溃。

这就提出了一个经典的设计困境:你是将应用程序构建为一组隔离的、健壮的进程,还是一个单一的、高性能的多线程进程?答案是在故障隔离和性能之间的权衡。我们甚至可以量化这一点。想象一个“故障爆炸半径”——当一个任务失败时必须终止的任务数量。对于多线程应用程序,爆炸半径是线程总数。对于由单任务进程构建的应用程序,爆炸半径仅为一。通过对通信的性能开销与期望的弹性水平进行建模,系统设计者可以做出有原则的选择,有时甚至选择一种混合模型,将几个线程分组到多个进程中,以寻求在安全和速度之间取得一个最佳平衡点。

互动与通信的法则

在一个由进程构成的世界里,必须有互动的法则。它们如何交换信息?这就是​​进程间通信(IPC)​​的角色。IPC机制的演进是操作系统更广泛演进压力的一个完美缩影。

早期的系统,以及许多现代系统,都倾向于​​消息传递​​。一个进程将其数据打包成一条消息,并请求内核传递它,就像使用邮政服务一样。使用像套接字这样的机制,生产者进程写入其数据,内核将其复制到自己的受保护内存中。然后它再将数据复制到消费者进程的内存中。这种双重复制确保了安全和隔离,但它有成本。发送消息的延迟有一个固定部分(对内核进行系统调用的开销 σ\sigmaσ)和一个可变部分,取决于消息的大小(xxx)和内存复制的带宽(BkB_kBk​)。总时间大约是 Tsocket(x)≈nsσ+2xBkT_{\text{socket}}(x) \approx n_s \sigma + 2 \frac{x}{B_k}Tsocket​(x)≈ns​σ+2Bk​x​。

随着应用程序变得越来越数据密集型,处理器速度越来越快,这种双重复制的开销成为了一个显著的瓶颈。解决方案是什么?向​​零拷贝共享内存​​演进。进程们不再使用邮政服务,而是同意共享一个“公告板”——一个映射到它们两个地址空间的内存区域。生产者将数据写入公告板,消费者直接读取。内核只参与设置共享空间,或许还帮助进行同步(例如,当有新数据时唤醒消费者)。数据本身从未被内核复制。这种方法通常涉及更多的系统调用以进行协调(nsh>nsn_{\text{sh}} > n_snsh​>ns​),但其数据传输成本要低得多,主要由缓存到缓存的传输速度(BccB_{cc}Bcc​)决定。其延迟看起来像 Tshmem(x)≈nshσ+xBccT_{\text{shmem}}(x) \approx n_{\text{sh}} \sigma + \frac{x}{B_{cc}}Tshmem​(x)≈nsh​σ+Bcc​x​。

哪种更好?没有一种是普遍最优的。通过令两种延迟相等,我们可以解出一个阈值消息大小 x⋆x^{\star}x⋆,在该大小下两种方法的性能持平。对于小于 x⋆x^{\star}x⋆ 的消息,系统调用的固定成本占主导地位,使得更简单的套接字方法更快。对于大于 x⋆x^{\star}x⋆ 的消息,每字节的复制成本占主导地位,使得零拷贝共享内存成为明显的赢家。操作系统设计的演进充满了这样的权衡,新机制的出现不是为了取代旧机制,而是为了提供针对设计空间中不同点进行优化的新选项。

持续的斗争:并发、一致性与安全

随着操作系统变得越来越复杂,它们面临着一系列持续不断的、反复出现的挑战,这些挑战至今仍在推动其演进。

多核世界中的并发

从单处理器到多处理器系统的转变是一场地震。几十年来,内核开发者生活在一个相对安全的世界里。在单处理器上,只要你暂时禁用中断,就可以保证一段内核代码不会与另一段交错执行。随着多处理器(对称多处理或​​SMP​​)的出现,这种保证烟消云散。真正的并行性到来了,随之而来的是一个潘多拉魔盒,装满了各种微妙的​​竞争条件​​。

考虑一个简单的任务:计算一个线程在内核中执行系统调用所花费的时间。一个简单的设计可能是在入口处记录一个时间戳,在出口处减去它。现在,让内核变得​​可抢占​​,这意味着一个线程在其内核执行的任何时刻都可能被停止,以让更高优先级的线程运行。会出什么问题呢?

  • ​​时间丢失:​​ 如果一个线程在记录开始时间和设置一个表示“我在内核中”的标志之间被抢占,一个定时器中断可能会发生,并将该时间片错误地归于用户,因为那个标志还没有被设置。
  • ​​时钟偏斜:​​ 如果线程在CPU 1上被抢占,迁移并在CPU 2上恢复,它可能会通过从CPU 2的时钟获取的结束时间减去CPU 1时钟的开始时间来计算其执行时间。由于这些每个CPU的时钟并非完美同步,得出的持续时间可能会非常不准确,甚至可能是负数。
  • ​​重复计数:​​ 操作系统可能使用两种计算方法:包围法(结束时间减去开始时间)和周期性采样法(一个定时器滴答检查线程是否在内核中,并增加一个滴答的时间量)。没有仔细的同步(在不可抢占代码中是隐式提供的),一个定时器滴答可能会发生,并计入一段也被最终包围法计算所包含的时间,导致重复计数。驯服并发需要极大的纪律、新的同步原语(锁、互斥锁),以及对任何两行代码之间可能发生的事情的深度偏执。

面对失败时的一致性

数字宇宙并非对灾难免疫。电力可能在任何时刻中断。操作系统如何确保其结构,特别是文件系统,保持一致?一个简单的文件写入可能涉及多个、独立的磁盘操作:写入数据本身,然后更新元数据(如文件大小、修改时间和指向新数据的指针)。如果两次操作之间发生崩溃,文件系统将处于损坏的、不一致的状态。

对此的演进答案是​​日志文件系统​​。其核心思想借鉴自会计学:在对主账本进行任何更改之前,你首先将你打算进行的交易记录在一个单独的日志中,即​​journal​​。一旦交易安全地记录在日志中,你就可以将更改应用到账本本身。如果发生崩溃,你可以在重启时简单地查看日志,并“重放”任何已完成但尚未应用的交易,从而使系统恢复到一致状态。

这个简单的想法有不同的风格,每一种都代表了性能和安全之间的不同权衡。

  • ​​回写模式(Writeback mode)​​是最快的:只有元数据更改被写入日志。数据本身则在方便的时候被写入其最终位置。它速度快,但在日志提交后、数据写入前发生崩溃可能导致文件充满旧的或垃圾数据。
  • ​​有序模式(Ordered mode)​​是一种更安全的折衷方案:它强制数据在其对应的元数据提交到日志之前被写入其最终位置。这可以防止出现垃圾数据的情况。
  • ​​数据=日志模式(Data=journal mode)​​是最偏执的:它将元数据和数据都写入日志。这提供了最强的一致性保证,但代价是所有数据都要写两次(一次写入日志,一次写入其最终位置)。 选择一种日志模式就是在持久性-性能曲线上选择一个点,这是一个由预期工作负载和期望的可靠性驱动的决策。

敌对世界中的安全

最后,操作系统必须是一个警惕的守护者。关于如何实施保护,有两大哲学流派。占主导地位的模型,见于Unix及其后代(​​POSIX​​)等系统中,是基于​​访问控制列表(ACLs)​​的。在这里,权限与你的身份绑定。当一个进程(代表用户行事)试图打开一个文件时,操作系统会检查文件的ACL,看该用户的ID是否被允许执行该操作。这被称为​​环境权限(ambient authority)​​:进程随身携带其权力,就像一个国王可以发布任何允许国王发布的命令一样。这可能很危险,正如在setuid等机制中所见,程序临时承担了另一个用户的全能身份,这是安全漏洞的一个常见来源。

另一种哲学见于​​基于能力(capability-based)的系统​​。在这里,权限不是环境性的;它是明确且细粒度的。要访问一个对象,进程必须拥有一个​​能力(capability)​​——一个不可伪造的令牌,就像一把有特定使用规则的特定门的钥匙(例如,“这把钥匙可以打开文件X,但只能用于读取”)。你不能做任何你没有钥匙的事情。要让另一个进程做某事,你不是把你的身份借给它;你是给它一把特定钥匙的副本。这更自然地遵循了​​最小权限原则​​。虽然更纯粹的能力系统仍然是少数,但它们的思想深刻地影响了现代操作系统设计,推动了更明确、可委托的权限形式。

除了这些确定性模型之外,一类新的基于概率的防御措施也已演进。如果攻击者确切知道一个库在内存中的加载位置,他们就可以制造依赖该知识的漏洞利用。​​地址空间布局随机化(ASLR)​​通过为每个新进程在随机地址加载系统组件来挫败这一点。操作系统实际上是将其关键组件隐藏在 M=2HM = 2^HM=2H 个可能位置中的一个,其中 HHH 是熵的比特数。攻击者只能靠猜测。这有多有效?我们可以用经典的​​生日悖论​​来建模。对于 nnn 个进程,“碰撞”(两个进程意外获得相同随机地址)的概率大约是 1−exp⁡(−n(n−1)/2M)1 - \exp(-n(n-1)/2M)1−exp(−n(n−1)/2M)。对于一个拥有 H=28H=28H=28 比特熵的系统,即使有 n=10,000n=10,000n=10,000 个正在运行的进程,单次碰撞的概率也出人意料地高,约为 0.170.170.17。这提醒我们,虽然随机化是一种强大的防御,但其有效性关键取决于可用熵的数量。

对全知的追求:可观测性的兴起

操作系统的旅程远未结束。随着系统变得分布式、虚拟化和层次化,其复杂性与日俱增,一个新的前沿已经出现:​​可观测性​​。操作系统仅仅能工作已经不够了;我们必须能够问它:“你现在在做什么,为什么?”

早期的操作系统提供粗糙的日志记录。现代系统已经演化出复杂的动态跟踪框架,如​​DTrace​​和​​eBPF​​。这些工具允许开发人员和管理员安全地在内核的几乎任何地方插入自定义探针,以最小的开销实时观察系统行为。这代表着操作系统变得具有自我意识。

这种演进本身可以被建模为一个优化问题。可观测性的价值 V(s)V(s)V(s) 是采样率 sss 的函数。它呈现出收益递减的特点——最初的几个探针能给你巨大的洞察力,但第一百万个探针告诉你的就少了。探测的成本 κ(s)\kappa(s)κ(s) 往往会随着高密度探针开始导致资源争用而超线性增长。净收益是 F(s)=V(s)−κ(s)F(s) = V(s) - \kappa(s)F(s)=V(s)−κ(s)。利用微积分,我们可以找到最佳采样率 s⋆s^{\star}s⋆,在该点上,再增加一个探针的边际效益恰好等于其边际成本。高效跟踪框架的兴起,是一个大幅降低成本函数 κ(s)\kappa(s)κ(s) 的故事,使我们能够将 s⋆s^{\star}s⋆ 推得更高,从而对我们数字宇宙的内部运作获得前所未有的洞察。

从映射内存到验证自身完整性,从构建优雅的抽象到驯服并行与失败的混乱,操作系统的演进证明了人类在一个本质上复杂的世界中建立秩序、美和安全的驱动力。这是一个仍在书写的故事,每一个新的挑战都将我们推向更巧妙的原理和机制。

应用与跨学科联系

操作系统的原理,就像物理定律一样,不仅仅是局限于教科书中的抽象规则。它们是我们数字世界无形的建筑师,是将人类意图转化为计算现实的无声引擎。在经历了操作系统核心机制的运作原理之旅后,我们现在将注意力转向魔法真正发生的地方:在它的应用中,以及它与其他科学和工程领域的深层联系。操作系统的演进是一个解决日益复杂问题的过程,一段从简单的监督者到复杂的、遍布全球的平台的旅程。

从全球公地到私有世界

想象一个有着单一共享牧场的小村庄。当只有少数几个农民时,这还算行得通。但随着村庄的成长,冲突变得不可避免。谁的羊可以在哪里吃草?你如何阻止一个人的项目毁掉另一个人的?早期的操作系统在文件和资源的单一全局“命名空间”方面面临着完全相同的问题。解决方案,已经成为操作系统演进的一个决定性主题,是数字私有财产的发明:隔离。

第一步非常简单:给每个用户自己的“家”,一个存放他们文件的个人目录。这是早期多用户系统(如Unix)中经典的双层目录系统。在你自己的目录里,你基本上可以随心所欲,这是一种由我们所谓的自主访问控制(DAC)管理的模式,即文件的所有者是其国王。

但随着我们的数字社会变得更加复杂,这还不够。你家里面的应用程序怎么办?一个在你名下运行的有缺陷或恶意的程序会破坏你的其他文件吗?你手机上的现代移动操作系统给出了答案。每个应用都生活在自己戒备森严的“沙箱”里,一个它无法逃脱的私有目录。这是一个由强制访问控制(MAC)支配的世界,其中一个更高的系统级策略决定了什么是允许的,无论谁“拥有”该文件。没有明确的、经过中介的许可,应用程序根本不被允许看到其沙箱之外的东西。这种从以用户为中心的“家”到以应用为中心的“沙箱”的演进飞跃,反映了从管理便利性到强制执行安全性的深刻转变。

这种对隔离的追求不仅仅关乎安全;它还关乎管理复杂性。在一个假设模型中,如果我们能够量化名称冲突——两个不同组件意外尝试为资源使用相同名称——的“成本”,我们会发现,增加隔离级别会显著降低这种成本。通过划分出更多的私有命名空间,从进程到容器,现代系统使得构建复杂软件而无意外干扰变得指数级地容易。操作系统从一个城镇规划者演变成了整个自给自足宇宙的建筑师。

不可破坏的契约及其细则

一个操作系统要成为一个有用的基础,它必须做出承诺。它必须与运行在其上的应用程序签订一份契约,一份让软件世界能够在不持续崩溃的情况下前进的契约。

也许这些承诺中最神圣的是应用程序二进制接口(ABI)的稳定性。这是操作系统的庄严誓言:你多年前编译的程序在最新版本的系统上仍然能正确运行。想象一下,如果每次你的城市升级电网,你都必须重新给你家里的每个电器布线!操作系统通过巧妙的工程设计避免了这种混乱。当内核开发者需要更改内部数据结构时——例如,在返回的文件信息中添加新信息——他们不会强迫每个人都去适应。相反,他们会构建一个“兼容层”。操作系统内核学会识别来自旧程序的调用,并透明地将它们老式的请求转换为新格式,然后再将结果转换回去。这使得操作系统能够在维护与过去的契约的同时,演进和改进其内部机制,这是抽象在实践中的一个美丽例子。

另一个基本承诺是原子性。当你请求操作系统做一些简单的事情,比如用 rename() 系统调用重命名一个文件时,操作系统保证该操作将原子地发生——它要么完全完成,要么完全失败,使系统如同什么都没发生过一样。不存在文件有两个名字或根本没有名字的混乱中间状态。这个保证是数据完整性的基石。

然而,每个魔术都有其局限性。这种强大的原子性幻觉通常仅限于单个文件系统。如果你试图将一个文件从你的主硬盘 rename() 到一个U盘,你就跨越了两个独立世界之间的边界。操作系统无法保证在这两个域之间进行原子操作。它不会假装可以,而是优雅地退出,返回一个错误(EXDEV,表示“跨设备链接”)。这时就得由应用程序来用更困难的方式执行移动:通过手动复制数据,验证副本,然后删除原始文件。这个后备序列,至关重要的是,不是原子的。一次崩溃可能会让你得到文件的两个副本,或者如果在错误的时机发生,一个副本都没有。这揭示了关于操作系统的一个深刻真理:它们是抽象的大师,但它们的智慧的一部分在于知道自己力量的边界。

对话的艺术与并发之舞

应用程序和操作系统内核之间的边界是计算机中至关重要的边界。通过系统调用跨越它不像一次随意的交谈;这是一种正式的、受到高度审查的互动。随着操作系统的演进,这个接口的“边境巡逻”变得异常复杂,其驱动力是保护内核免受有缺陷或恶意应用程序的侵害。

考虑一个现代系统调用的设计,也许是一个对许多文件进行批量操作的调用。内核不能简单地信任应用程序提供的文件列表。它必须执行一个严格的安全检查清单:指向列表的指针是否有效?文件数量是否大得可疑,可能耗尽内核内存?每个单独的文件路径长度是否合理?如果一百个操作的批次中的第三个操作失败了会怎样?一个健壮的系统调用必须被设计来处理所有这些情况,提供清晰的、逐项的错误报告,而绝不使系统崩溃。这种细致的设计是几十年来防御性编程和安全接口设计演进的直接结果。

当我们考虑并发——多件事情同时发生时,这种复杂性被放大了。在操作系统内部,不同的执行线程必须以惊人的精度进行协调。一个经典而优美的例子出现在设备驱动程序中。想象一个线程开始一个硬件操作,比如直接内存访问(DMA)传输。它获取一个配置锁(LcfgL_{cfg}Lcfg​)来保护其数据结构,然后进入睡眠,等待硬件发出完成信号。信号以中断的形式到达,这是一个独立的、高优先级的执行线程。致命的拥抱就在这里:中断处理程序需要获取同一个锁 LcfgL_{cfg}Lcfg​,来更新共享数据并唤醒休眠的线程。但是休眠的线程正持有这个锁!线程在等待中断,而中断在等待线程。这就是死锁。

解决方案是一段优雅的编排。线程必须在进入睡眠之前释放锁。但这会产生一个新的竞争:如果中断在锁被释放之后、线程进入睡眠之前的微小窗口内到达怎么办?这是一个“唤醒丢失”,线程将永远沉睡。经过多年艰苦经验发展的正确模式是基于谓词的等待。线程释放锁,然后仅当一个完成标志尚未被设置时才进入睡眠。中断处理程序则负责设置标志,然后唤醒任何可能正在睡眠的线程。这个复杂的舞蹈通过打破“持有并等待”条件来确保正确性并消除死锁。

现代世界中的操作系统:分布式、实时与共生

对操作系统的演进压力并未停止。今天,它们正在适应一个远远超出单台计算机的世界,一个具有极端性能要求和系统与应用之间复杂相互作用的世界。

以我们简单的 rename() 操作为例。当我们在一个分布式文件系统中尝试执行它时会发生什么?在这个系统中,文件存放在世界各地的服务器上,各地的客户端都缓存了目录结构的副本。问题变得异常复杂。为了防止客户端使用一个过时的、“孤儿”路径,系统必须采用一个复杂的协议。在提交重命名之前,服务器必须获取源目录和目标目录的锁,然后同步地向所有缓存这些条目的客户端发送失效通知,并等待它们的确认。因为元数据本身可能分散在多个服务器上,整个操作必须被包裹在一个像两阶段提交这样的分布式事务中,以确保其保持原子性。rename() 的简单、本地承诺演变成了一个复杂的、分布式的共识算法。

在另一个极端,考虑一个具有极端性能要求的应用程序,比如专业的音频工作站。音频处理线程必须每隔几毫秒就无差错地交付一个新的音频缓冲区。一个单一的故障就可能毁掉一次录音。这个线程在近乎硬实时的约束下运行。现在,如果用户想要加载一个新的音频效果插件怎么办?操作系统对此的标准机制是动态加载(dlopen()),这是一个强大但完全非实时的操作。它可能需要从慢速磁盘读取、分配内存或等待一个全局锁——任何一个都可能导致它错过毫秒级的最后期限。

解决方案是操作系统和应用程序之间协同设计的一个美丽范例。应用程序将自己分裂成两个角色。一个非实时的“控制线程”处理调用 dlopen() 和设置插件这些缓慢、不可预测的工作。一旦插件完全加载并准备就绪,它就通过一个专门的无锁通信通道被移交给对时间要求严格的音频线程。音频线程本身从不获取锁,从不分配内存,当然也从不调用 dlopen()。它生活在一个由其助手线程为它创造的、纯净的、确定性的世界里,这完美地说明了现代应用程序如何适应并围绕操作系统的通用性进行工作。

这引出了我们一个最终的、前瞻性的想法:操作系统及其应用程序演进成为一种真正的共生关系。在一个拥有托管运行时(如Java或Go)的系统中,应用程序有自己的内存管理器:一个垃圾回收器(GC)。操作系统也有自己的:请求分页系统。当机器内存不足时,操作系统可能会开始积极地回收页面,导致系统停顿。与此同时,应用程序的运行时可能正在考虑运行自己的GC。性能工程中的一个引人入胜的洞见是,这两个系统可以合作。通过对GC暂停与操作系统引起的分页停顿的成本进行建模,应用程序的运行时有可能选择一个最佳时机,执行恰到好处的垃圾回收来缓解操作系统的内存压力,从而为最终用户最小化总延迟。这是对未来的惊鸿一瞥:一个生态系统,其中操作系统和它运行的程序不仅仅是宿主和客人,而是为实现共同目标而协同工作的智能伙伴。

从第一个私有目录到对全球资源的智能、协作管理,操作系统的演进证明了抽象的力量、稳健设计的必要性,以及对构建更强大、更安全、更优雅的计算世界的不懈追求。