try ai
科普
编辑
分享
反馈
  • 计算机系统体系结构

计算机系统体系结构

SciencePedia玻尔百科
核心要点
  • 计算机体系结构本质上是关于管理权衡,例如在性能与灵活性、成本与可靠性以及速度与安全性之间取得平衡。
  • 由指令集和内存模型定义的硬件-软件接口,作为一个关键契约,既实现了高效执行,也带来了潜在的安全漏洞。
  • 存储器层次结构,包括高速缓存和虚拟内存,是一个旨在弥合CPU与主存之间速度差距的幻象系统,它决定了整个系统的性能。
  • 体系结构的选择具有深远的跨学科影响,直接影响着操作系统设计、应用程序性能瓶颈以及网络安全防御策略。

引言

计算机系统体系结构是所有现代计算的基础蓝图,它定义了规则和结构,使得软件的抽象逻辑能够在物理硬件中得以实现。对许多人来说,计算机的内部工作原理是一个无法穿透的黑箱,然而,理解其核心原理对于任何希望精通开发高效、可靠和安全系统的人来说至关重要。本文通过剥开抽象的层层面纱,揭示其核心的优雅思想,从而揭开这台机器的神秘面纱。

本次探索分为两个部分。首先,在“原理与机制”部分,我们将深入探讨计算机的基本构建模块。我们将学习它的原生二进制语言,探索驱动其“思考”的布尔逻辑,并审视CPU、存储器和I/O设备等核心组件是如何被组织和调度的。随后,“应用与跨学科联系”部分将拓宽我们的视野,展示这些基础架构决策如何向外涟漪般地影响软件的性能、操作系统的设计以及计算机安全的本质,揭示硬件与其驱动的数字世界之间深刻而复杂的共舞。

原理与机制

要真正理解一台计算机,我们必须剥开抽象的层层面紗,窥探机器的核心。我们必须学习它的母语,理解它的逻辑,并欣赏其组件之间错综复杂的协作。这并非为了探究而探究的奥秘之旅;这是一场发现之旅,旨在揭示那些赋予我们日常使用的强大功能的根本原理和优雅机制。就像物理学家揭示支配复杂宇宙的简单定律一样,我们将会发现,现代计算机令人眼花缭乱的复杂性,是建立在一些惊人地简单而优美的思想基础之上的。

计算的字母表

在进行计算之前,我们必须先有表示方法。人类有字母、数字和符号。而计算机只有一样东西:电信号的有或无。我们称之为一个​​比特​​(bit),并用111和000来标记它的两个状态。每一条信息——每一个数字、字母、图片和声音——都必须用这种朴素的二进制字母表进行编码。

想象一下,你被要求用简单的LED灯设计一个数字时钟。表示像一天中的小时这样的数字,最直接的方法是使用​​纯粹的位置二进制​​系统,这与我们的十进制系统工作方式相同。在十进制中,数字23表示(2×101)+(3×100)(2 \times 10^1) + (3 \times 10^0)(2×101)+(3×100)。在二进制中,同样的数字23被写作101111011110111,表示(1×24)+(0×23)+(1×22)+(1×21)+(1×20)(1 \times 2^4) + (0 \times 2^3) + (1 \times 2^2) + (1 \times 2^1) + (1 \times 2^0)(1×24)+(0×23)+(1×22)+(1×21)+(1×20),即16+0+4+2+116 + 0 + 4 + 2 + 116+0+4+2+1。要显示从000到232323的任何小时,你需要555个LED灯,分别代表权重1,2,4,8,161, 2, 4, 8, 161,2,4,8,16。要显示从000到595959的分钟,你需要666个LED灯,代表权重1,2,4,8,16,321, 2, 4, 8, 16, 321,2,4,8,16,32。这种方法在比特的使用上效率最高;它是机器的母语。

然而,这并非唯一的方法。例如,我们可以对每个十进制数字分别编码,这种方案被称为​​二进制编码的十进制(BCD)​​。要表示23,我们会将'2'编码为二进制(001000100010),将'3'编码为二进制(001100110011),总共需要888个比特而不是555个。虽然在硬件使用上效率较低,但BCD可以简化在基于十进制的屏幕上显示数字的逻辑。在这里,我们遇到了计算机体系结构中的第一个伟大主题:没有唯一的“最佳”解决方案。存在的只有权衡——在这里,是在纯二进制的硬件效率与BCD的潜在便利性之间的权衡。

思考的逻辑

一旦我们能够表示信息,下一步就是处理它。这是​​布尔代数​​的领域,即关于111和000、真与假的数学。仅通过三个基本运算——与(AND, ⋅\cdot⋅)、或(OR, +++)和非(NOT)——我们就能构建出任何可以想象的逻辑函数。这些并非抽象的数学奇珍;它们由称为​​逻辑门​​的简单电子电路物理实现。

考虑一个保持处理器流水线顺畅流动的问题。流水线就像指令的装配线。一个常见的问题,即​​写后读(RAW)冒险​​,发生在一条新指令需要读取一条先前仍在执行中、尚未完成写入的结果时。处理器必须检测到这种冒险并暂停流水线以防止出错。

检测此类冒险的逻辑可以表述如下:如果执行(EX)阶段指令的目的地与当前指令的源匹配,或者如果访存(MEM)阶段的指令也是如此,或者如果写回(WB)阶段的指令也是如此,那么冒险DDD就存在。令RDRDRD为一个信号,当寄存器匹配时为111,此逻辑为:

D=(WB⋅RD)+(MEM⋅RD)+(EX⋅RD)D = (WB \cdot RD) + (MEM \cdot RD) + (EX \cdot RD)D=(WB⋅RD)+(MEM⋅RD)+(EX⋅RD)

这个表达式完全正确,但布尔代数告诉我们,我们可以做得更好。使用分配律,就像在普通代数中一样,我们可以提出公因子RDRDRD:

D=(WB+MEM+EX)⋅RDD = (WB + MEM + EX) \cdot RDD=(WB+MEM+EX)⋅RD

这为什么重要?第一个表达式需要三个与门和一个或门。而第二个简化后的表达式只需要一个或门和一个与门。通过应用一个简单的逻辑定律,我们设计出了一个更小、更便宜、更快的硬件电路。这就是计算机体系结构之美:抽象的数学优雅直接转化为切实的物理效率。

组装机器:组件的交响曲

以逻辑门为构建模块,我们可以开始构建计算机的主要组件:中央处理器(CPU)、存储器和输入/输出(I/O)设备。它们的协调运作就像一首交响曲,而CPU的​​控制单元​​就是它的指挥。

指挥家:控制单元

控制单元的工作是解释指令并生成执行它们所需的精确信号序列。一个基本的设计选择决定了这个指挥家如何运作。一种方法是​​硬布线控制单元​​,其中逻辑被直接蚀刻在固定的电路中。它速度极快且高效,但也僵化且不可更改。

另一种选择是​​微程序控制单元​​。在这里,每条机器指令都由存储在称为控制存储器的特殊内存中的一系列“微指令”来解释。这就像为每个乐章给指挥家一份更详细的乐谱。这种方法提供了巨大的灵活性;如果在处理器制造后发现指令逻辑中存在错误,微程序可以被更新或“打补丁”。然而,这种灵活性是有代价的。获取和解码微指令的额外步骤增加了开销,并且可能增加指令执行时间的可变性,特别是当某些指令在其微代码中有条件路径时。在快如闪电、专用的专家(硬布线)和较慢但适应性更强的通才(微程序)之间的选择,是性能与灵活性之间的经典权衡。

双存记:指令与数据

我们交响乐的“乐谱”——指令——和产生的“声音”——数据——都必须存储在存储器中。经典的​​冯·诺依曼(von Neumann)体系结构​​对两者使用单一、统一的存储器。这简单而灵活。然而,它也造成了一个瓶颈,因为CPU无法在同一时刻既取指令又加载数据;它们必须轮流使用通往存储器的单一路径。

​​哈佛(Harvard)体系结构​​提出了一个简单而强大的替代方案:为指令和数据设置独立的存储器和独立的路径。这允许CPU在为当前指令加载或存储数据的同时,获取下一条指令。这种并行性带来的性能增益可能非常显著。如果一个程序循环涉及获取fff个指令字和加载lll个数据字,一个统一的系统需要的时间与f+lf+lf+l成正比。而一个哈佛系统,两者并行进行,需要的时间与两者中较长者成正比,即max⁡(f,l)\max(f, l)max(f,l)。因此,相对加速比为: G=f+lmax⁡(f,l)G = \frac{f+l}{\max(f, l)}G=max(f,l)f+l​ 这个简单的方程式优雅地捕捉了并行性带来的深远性能优势,这一主题贯穿于所有现代计算机设计的始终。

与外设低语:I/O的艺术

CPU还必须通过I/O设备(如网卡、磁盘驱动器和键盘)与外部世界通信。这通常通过​​内存映射I/O(memory-mapped I/O)​​来实现,这是一种非常直接的机制,其中设备控制寄存器被设计成看起来就像是内存中的位置一样。

要启用一个设备,软件并非发出一个特殊的“启用”命令;它只是向一个特定的内存地址写入一个特定的比特模式。例如,向地址0xFF00写入十六进制值0x0001可能会设置控制寄存器的第0位,从而打开设备。写入0x0004可能会设置第2位,触发硬件复位。硬件反过来可以通过在同一地址设置只读状态位来进行通信,指示它是否繁忙或遇到了错误。一种特别巧妙的设计模式是“自清除”位;软件写入一个1来触发复位,硬件在复位完成后自动将该位清除回0。这是软件-硬件契约最原始、最美丽的形式:程序员与硅片之间直接的、比特级的对话。

宏大的幻象:存储器层次结构

如果CPU是大脑,那么存储器就是其知识的源泉。但是主存速度很慢——从高速处理器的角度看,简直是永恒。为了弥合这种速度差距,架构师们创建了一个​​存储器层次结构​​:一系列位于CPU和主存之间的更小、更快、更昂贵的存储器。这个层次结构协同工作,创造出一个强大的幻象:为每个程序提供一个巨大、快速、私有的内存空间。

为每个程序创造一个私有宇宙

在现代计算机上运行的每个程序都相信自己独占了整个内存空间。这就是​​虚拟内存​​的魔力。实际上,程序被分配到物理内存中零散的区块。由操作系统管理的硬件会动态地将程序的“虚拟地址”转换为“物理地址”。

这种转换是通过一组称为​​页表​​的映射来完成的。当程序请求某个虚拟地址的数据时,硬件首先检查一个名为​​转译后备缓冲器(TLB)​​或​​快表​​的、用于缓存近期翻译的小型、极速缓存。如果翻译信息在那里(TLB命中),访问就很快。如果不在(TLB缺失),硬件必须执行一次“页表遍历”。它从一个特殊的CPU寄存器(PTBR)中读取页表的基地址,使用虚拟地址计算出该表中的索引,然后执行​​第一次内存读取​​来获取正确的页表项(PTE)。这个PTE包含了数据的物理位置。之后,硬件才能执行​​第二次内存读取​​,最终获取到程序想要的数据。TLB缺失时的这两次读取代价,正是为每个进程提供私有地址空间这一强大抽象所付出的代价。

用高速缓存加速

TLB是地址专用的一种高速缓存。更普遍地说,​​高速缓存(cache)​​用于存储最近访问的数据和指令。当CPU需要一块数据时,它首先检查高速缓存。如果数据在那里(​​命中​​),访问就非常快。如果不在(​​缺失​​),CPU必须忍受从主存中获取数据的漫长等待(​​缺失代价​​)。整体性能由​​平均内存访问时间(AMAT)​​来衡量:

AMAT=命中时间+(缺失率×缺失代价)\text{AMAT} = \text{命中时间} + (\text{缺失率} \times \text{缺失代价})AMAT=命中时间+(缺失率×缺失代价)

这个公式是存储系统性能的基石。但速度并非唯一的考量。可靠性呢?高能粒子可能会翻转内存中的比特,导致静默的数据损坏。为了对抗这一点,系统可以使用​​纠错码(ECC)​​,它为每个数据块添加额外的比特以检测和纠正错误。

然而,这种可靠性并非没有代价。检查和纠正比特的逻辑会给每次高速缓存访问增加微小的延迟,略微增加了命中时间。它也为在缺失时获取数据的过程增加了开销,增加了缺失代价。虽然这些性能损失看似不受欢迎,但必须权衡其带来的好处。为了AMAT上可能仅几分之一纳秒的微小增加,ECC可以将未检测到错误的概率降低数千倍。这揭示了体系结构的另一个深刻真理:设计是一种多目标优化,是在性能、成本、功耗和可靠性之间寻求精妙平衡的行为。

前沿:并发、通信与安全

我们讨论的原理构成了计算的基础,但该领域为了应对新挑战在不断进步。体系结构的前沿由管理大规模并行、确保正确通信以及防御新型攻击的需求所定义。

中断的艺术

计算机必须对不可预测的外部世界做出响应。当一个I/O设备,如网卡,收到一个数据包时,它不能等待CPU来询问。它会用一个​​中断​​来通知CPU。CPU会立即暂停当前的工作,跳转到一个称为​​中断服务例程(ISR)​​的特殊函数来处理该事件。

当所需的工作很长时,一个关键的设计挑战就出现了。如果一个低优先级设备的ISR耗时过长,它可能会延迟对一个更高优先级设备中断的处理,这种情况被称为​​优先级反转​​。解决方案是一种优雅的分工。ISR,即“上半部”(top half),只做绝对必要的最少量工作——也许只是将传入的数据复制到一个队列中——然后迅速返回。更长、更复杂的处理被推迟到“下半部”(bottom half)或延迟过程调用,由操作系统调度为常规软件线程稍后运行。这种分离式设计确保了系统对紧急中断保持高度响应性,同时仍能执行复杂的工作,这是硬件即时性与软件调度灵活性之间的一场优美共舞。

核心议会

现代处理器几乎都是​​多核​​的,在单个芯片上包含多个独立的CPU。这带来了巨大的处理能力,但也带来了一个严峻的挑战:这些核心如何共享数据而不会陷入混乱?如果两个核心试图同时更新同一个内存位置,数据可能会被损坏。硬件必须提供机制来确保​​原子性​​——即对共享数据的操作看起来是不可分割地发生的。

对于跨越多个内存位置的操作,这一点尤其困难,这些位置可能由芯片的不同部分管理。想象一下试图原子地更新两个变量AAA和BBB。核心1可能试图先锁定AAA再锁定BBB,而核心2试图先锁定BBB再锁定AAA。它们可能会陷入​​死锁​​,每个核心持有一个锁并永远等待另一个。为了解决这个问题,硬件可以实现一个类似于礼仪规则的分布式协议。所有核心必须同意以全局一致的顺序获取锁——例如,总是先锁定地址较低的内存行。这个简单的顺序规则打破了循环依赖,防止了死锁,使得一个由众多核心组成的“议会”能够协同工作而不会陷入停顿[@problemid:3635526]。

机器中的幽灵

对性能的不懈追求催生了强大的技术,如​​推测执行​​,即CPU对程序下一步将做什么进行有根据的猜测,并提前执行指令。如果猜对了,性能得到提升;如果猜错了,结果就被丢弃。同时,​​同时多线程(SMT)​​允许单个物理核心扮演两个虚拟核心的角色,共享资源以提高利用率。

近年来,人们发现这些优化存在一个黑暗面。SMT使用的共享硬件可能 tạo ra một "kênh bên" cho phép một luồng độc hại theo dõi các hoạt động suy đoán của một luồng khác đang chạy trên cùng một lõi。通过观察在另一个线程推测执行期间哪些部分的缓存被访问,攻击者可以推断出秘密数据,导致像Spectre和Meltdown这样的漏洞。

这迫使人们对基本的设计选择进行痛苦的重新评估。禁用SMT可以显著降低风险,但也会导致可观的性能下降。如何决策?这不再仅仅是一个工程问题;这是一个风险管理问题。决策可能由一个​​效用函数​​来指导,该函数权衡性能的 fractional 损失 (ΔIPC\Delta \text{IPC}ΔIPC) 与安全风险的 fractional 降低 (ρ\rhoρ),使用一个偏好参数 α\alphaα:

U=α(1−ΔIPC)+(1−α)ρU = \alpha (1 - \Delta \text{IPC}) + (1 - \alpha) \rhoU=α(1−ΔIPC)+(1−α)ρ

通过找到一个让人在两种选择之间无所谓的 α\alphaα 值,一个组织可以就其安全态势做出理性的、量化的决策。这就是计算机体系结构的现代现实:逻辑和性能的优雅原则现在与复杂、对抗性的安全世界相交织,迫使我们不仅要问“我们如何能让它更快?”,还要问“这种速度的代价是什么?”

应用与跨学科联系

窥探了现代处理器错综复杂的内部结构之后,我们可能很容易将计算机体系结构视为一门专业的、封闭的学科,一个由逻辑门、高速缓存和流水线构成的世界。但事实远非如此。体系结构的原理并不仅限于芯片之内;它们是整个数字世界赖以构建的物理定律。一位架构师所做的选择——关于指令集、存储系统或安全特性——会在软件的每一层中回响,塑造着从视频游戏的速度到互联网的安全,从操作系统的结构到云计算的经济模式等方方面面。

要真正欣赏体系结构之美,就要看到这些联系,追寻一个设计决策从一小片硅晶片一直传播到我们日常使用的复杂系统的涟漪效应。这是一段揭示计算领域深刻统一性的旅程,在这里,软件的抽象逻辑永远与硬件的物理现实进行着一场精妙的舞蹈。让我们踏上这段旅程,发现架构师的技艺如何在更广阔的世界中找到它的声音。

指令的艺术:锻造计算的工具

从本质上讲,处理器的指令集架构(ISA)就是它的词汇。它是硬件知道如何执行的一组基本操作——即“动词”。对于架构师来说,一个出人意料的深刻问题仅仅是:我们应该教处理器哪些词?

想象一下,你正在为一个复杂的应用程序编写软件,也许是一个国际象棋引擎或一个密码系统。你发现自己经常需要执行一个特定且常见的任务:计算一个64位数字中置位比特的数量(其“population count”)。你可以编写一个巧妙的软件例程,使用处理器已经知道的一系列(大约十几个)简单指令,如移位、掩码和加法。或者,你可以请求架构师添加一条新的、单一的指令——我们称之为POPCNT——一次性完成整个工作。哪种更好?

这不是一个学术问题;这是一个根本性的权衡。添加POPCNT指令需要将宝贵的硅片面积专用于一个专门的电路,使芯片更加复杂。软件例程不需要新的硬件,但它会消耗更多的时间和能量,可能成为一个瓶颈。架构师必须是一个精明的判断者,权衡硬件的成本与对重要软件的性能增益。通过对处理器的超标量流水线、其并行执行多条指令的能力以及每个操作的具体延迟进行建模,架构师可以精确计算出一条新指令对给定工作负载所能提供的加速比。事实证明,对于需要计算大量独立 population count 的任务,一个专用的硬件指令可以比其软件 counterparts 快得多,从而证明增加的复杂性是合理的。这种软件需求与硬件成本之间的持续对话,正是ISA设计的精髓所在。

但这种软件与硬件之间的语言不仅仅是提升性能的工具;它是一种契约,一套程序必须遵守的规则。这个契约最重要的部分之一是​​调用约定​​,它规定了函数之间如何相互调用。可以把它想象成打电话的礼仪。当一个函数调用另一个函数时,它会将一个“返回地址”——即调用结束后从何处恢复执行——保存在内存的一个称为栈的特殊区域。随着函数的调用和返回,栈会增长和收缩,形成一堆整齐的活动记录,每个记录都是函数调用的临时工作区。

这种有序的行为是正常执行的一个不变量。但如果它被违反了呢?这就是体系结构与​​计算机安全​​交汇的地方。许多最强大的软件攻击都是通过颠覆这种硬件-软件契约来起作用的。攻击者可能会发现一个漏洞,允许他们执行​​栈迁移(stack pivot)​​:覆写栈指针(SPSPSP)寄存器,使其从合法的栈指向一个攻击者控制的缓冲区,也许是在堆上。这就像一个恶意操作员劫持了你的电话,并将其重定向到他们自己的交换机。一旦栈被迁移,攻击者就有了一块白板,可以写入一串假的返回地址,从而劫持程序的控制流,执行他们自己的恶意代码。

我们如何防御这种情况?利用我们的体系结构知识!我们可以构建安全系统作为警惕的监视器,检查是否存在违反栈正常行为的情况。这些可以是软件启发式方法,例如检查栈指针是否突然移动到了像堆这样的无效内存区域。或者我们可以验证保存的帧指针链的完整性,确保它们在合法的栈区域内形成一个貌似合理的、单调变化的地址序列。一个更强大的防御措施涉及一种称为“影子栈(shadow stack)”的硬件特性,即处理器自己维护一个受保护的、第二份返回地址链。主栈和影子栈之间的任何不匹配都表明存在篡改,可以在攻击开始之前就将其阻止[@problemid:3670188]。在这里,我们看到了二元性的美:正是那些促成有序程序执行的体系结构规则,也为捍卫它提供了基础。

性能的引擎:对数据的无尽渴求

现代处理器是一个速度惊人的引擎,每秒能执行数十亿次操作。然而,这个引擎对数据有着贪婪的胃口。很多时候,这个强大的引擎都在空转、停滞,等待着数据从内存中送达。因此,存储器层次结构——由高速缓存、RAM和存储器组成的系统——的设计,不仅是计算机体系结构的一个辅助功能;在追求性能的道路上,它 arguably 是其最关键的方面。

一个极具直观性的可视化这种张力的方法是​​Roofline模型​​。想象一个图表,纵轴是计算性能(单位为Giga-Operations Per Second, GOPS),横轴是“算術強度”(操作次数与数据移动字节数的比率)。处理器有一个峰值计算性能,一个“计算屋顶”,代表了在数据能即时获得的情况下它可能运行的最快速度。但还有另一个倾斜的屋顶线,由内存带宽决定。这条线的斜率是内存系统供应数据的速率。一个程序的性能受限于这两个屋顶中较低的一个。如果一个算法的算術強度低(即每获取一个字节所做的计算很少),它将撞上倾斜的内存带宽屋顶,从而​​内存受限(memory-bound)​​。如果其强度高,它将撞上平坦的计算屋顶,从而​​计算受限(compute-bound)​​。这个简单的模型为架构师和程序员提供了一个强大的诊断工具。通过计算给定核心的这两个限制,人们可以立即识别性能瓶颈,并知道应该将优化工作集中在改善算法的数据局部性上,还是使用更强大的计算指令上。

存储系统的微妙影响出现在最意想不到的地方。考虑一个实现​​零拷贝I/O(zero-copy I/O)​​的高性能网络栈。这个名字暗示了一种完美的优化:CPU不接触网络数据包的有效载荷,而是指示网络接口卡(NIC)通过直接内存访问(DMA)直接从内存中获取它。这避免了用CPU永远不會使用的數據污染CPU高速緩存。但数据包的头部呢?CPU仍然必须为每个出站数据包写入以太网、IP和TCP头部。假设头部长度为66字节,而高速缓存以64字节的行工作。一个66字节的写入操作,如果它从一个64字节的边界开始,将不可避免地触及两个高速缓存行。由于NIC的DMA引擎通常与CPU高速缓存不一致,操作系统必须显式地“清洗”这些脏的缓存行,将它们的全部内容写回主存,以便NIC能看到这些变化。因此,对于每个66字节的头部修改,系统实际上向内存产生了2×64=1282 \times 64 = 1282×64=128字节的回写流量!这个隐藏的成本,是缓存行粒度的直接后果,可以在一个本应高度优化的系统中,造成一个重大且不明显的性能瓶颈。

存储体系结构的影响甚至决定了我们如何构建和共享软件。在现代操作系统中,让多个程序在内存中共享一个库(如标准C库)的单个副本是非常理想的。要做到这一点,库的代码必须是​​位置无关代码(PIC)​​,这意味着无论它被加载到内存的哪个位置,都能正确运行。这禁止代码包含绝对内存地址。但那样的话,它如何调用一个在编译时地址未知的外部函数呢?解决方案是一项精美的工程杰作,涉及一个过程链接表(PLT)和一个全局偏移表(GOT)。调用被重定向到PLT中一个称为“thunk”的小段代码。当一个函数第一次被调用时,这个thunk会从GOT中查找该函数的真实地址(由操作系统加载器填写)并跳转到它。然而,这种间接寻址带来了性能成本。处理器现在必须执行一次从GOT的加载和一次间接跳转,而不是一次单一的、直接的调用。这个序列引入了额外的流水线停顿,并且更容易导致分支预测错误。通过分析微体系结构的成本——GOT查找的缓存缺失代价和间接分支的预测错误代价——我们可以精确地量化这个基本的软件工程抽象所带来的开销。

宏大的交响曲:作为现代系统基础的体系结构

当我们从单个指令和内存访问的视角放大,我们看到体系结构为我们最复杂的软件系统提供了根基。操作系统(OS)本身就是一件与硬件紧密对话而设计的软件杰作。

OS的主要工作之一是多任务处理——通过在多个程序之间快速切换,创造出许多程序同时运行的假象。这种切换,称为​​上下文切换​​,不是没有成本的。它涉及保存当前进程的全部状态(寄存器、程序计数器)和加载下一个进程的状态。我们如何以一种可在不同机器间比较的方式来衡量这个成本?一位架构师可能不会用微秒来衡量成本,而是用一个更直观的单位:处理器在那段时间里本可以执行的有用指令数量。通过使用诸如处理器的时钟频率(fff)和其平均每指令周期数(CPICPICPI)等基本指标,我们可以计算出这个“指令等效成本”为(f×ts)/CPI(f \times t_s) / CPI(f×ts​)/CPI,其中tst_sts​是一次切换的时间。这为我们提供了一个标准化的、直观的OS开销度量,例如,揭示了在一台高端服务器上的上下文切换可能花费数千条指令,而在一个简单的微控制器上则花费少得多。

当考虑到必须处理随机性的系统时,系统设计与其他学科的联系变得更加清晰。想象一下,内核从设备接收I/O事件,并将它们放入一个共享的环形缓冲区中,供用户空间程序消费。消息以某个平均速率到达,但确切的时间是随机的。用户空间的处理程序处理它们,但其处理时间也各不相同。如果缓冲区太小,在一阵突发到达期间消息将被丢弃。如果太大,我们就会浪费内存。它应该多大才能保证,比如说,溢出概率小于0.01?这不再仅仅是一个编程问题;这是一个​​随机建模​​问题。该系统可以精确地建模为数学领域​​排队论​​中的一个“队列”。通过描述到达过程(例如,泊松过程)和服务过程(例如,指数分布),我们可以推导出一个封闭形式的方程,该方程给出满足我们可靠性目标所需的最小缓冲区大小,用到达率和服务率表示。这是一个 stunning 的例子,展示了如何使用严谨的数学来设计健壮的计算机系统。

也许现代计算机体系结构最引人注目的应用是​​虚拟化​​,即驱动云计算的技术。其目标是在单个物理机器上运行多个隔离的“客户机(guest)”操作系统,由一个“虚拟机监控器(hypervisor)”管理。早期的尝试纯粹用软件来实现,这既复杂又缓慢。突破来自于硬件支持,例如英特尔的​​扩展页表(EPT)​​。EPT在硬件中提供了第二层地址转换,允许客户机操作系统管理自己的页表(将虚拟地址转换为“客户机物理”地址),而虚拟机监控器则使用EPT安全地将客户机物理地址转换为真正的主机物理地址。这种优雅的两级方案效率极高,但也带来了新的挑战:当内存访问导致故障时会发生什么?故障是属于客户机的(例如,客户机程序需要的页面在其自己的磁盘上),还是属于虚拟机监控器的(例如,客户机认为在RAM中的页面实际上已被交换到主机的磁盘上)?硬件必须向虚拟机监控器提供一个清晰的信号,使其能够处理自己的故障,同时有效地让客户机处理自己的故障,从而最大限度地减少代价极高的“VM退出”(陷入虚拟机监控器)。设计处理这些嵌套故障的最佳策略是虚拟机监控器设计中的核心挑战,而硬件 cleanly 分离客户机故障和EPT违规的能力,正是现代高性能云计算成为可能的原因。

最后,计算的世界不再是单一的。我们生活在一个异构的体系结构生态系统中,从服务器中的x86_64到我们手机和笔记本电脑中的arm64。我们如何弥合这种分歧?同样,体系结构和操作系统层面的抽象提供了答案。现代​​容器​​技术允许一个应用程序与它的所有依赖项打包在一起。一个“多架构”镜像可以捆绑x86_64和arm64两个版本。当你运行容器时,运行时会智能地选择适合你主机的原生版本。但如果你强制在你的arm64笔记本电脑上运行x86_64版本呢?Linux内核通过一个名为binfmt_misc的巧妙特性,可以检测到外来二进制文件,并调用一个像QEMU这样的用户模式模拟器。QEMU随后动态地将x86_64指令翻译成arm64指令。当然,这会带来性能损失,但这是一个特定的损失:只有用户空间计算变慢了。当被模拟的程序进行系统调用时——例如,读取一个文件——QEMU将该调用传递给原生的arm64主机内核,后者以全速执行它。这种美丽的体系结构(ISA)、操作系统特性(容器、binfmt_misc)和系统软件(QEMU)的分层,实现了一种几年前难以想象的可移植性和灵活性,使我们能够无缝地在几乎任何机器上运行几乎任何软件。

从单个指令的逻辑到全球云基础设施,计算机体系结构的原理是基石。这是一个要求既有微观视角又有宏观视角的领域,既要欣赏单个晶体管的物理特性,又要理解其对一个拥有数十亿个晶体管的系统的影响。在追求性能、可靠性和安全性的过程中,它是一门发现自己与几乎所有其他计算和数学分支进行着持续、创造性对话的学科,这证明了其基本思想的统一力量。