
每一天,我们都在与现代计算的一项基本奇迹互动:多任务处理。我们无缝地同时运行浏览器、音乐播放器和复杂的应用程序,所有这些程序似乎都获得了我们计算机的全部注意力。这就提出了一个深刻的问题:一台处理器数量有限的机器,如何能处理看似无限数量的任务?答案并不在于同时做所有事情,而在于操作系统深处管理的一种巧妙而快速的错觉。本文将揭开这种错觉的幕布,探讨我们体验到的多任务计算机与其底层运行现实之间的差距。
为了揭示其复杂性,我们将首先探讨驱动多任务处理的核心原理和机制。你将了解到并发、不同的调度哲学,以及硬件在为每个程序创建隔离、受保护的环境中所起的关键作用。在此之后,我们将在第二章“应用与跨学科联系”中拓宽视野,看看这些基础思想如何向外扩散,影响从高性能计算、编程语言设计到硬件本身物理寿命的方方面面。
现代计算的核心是一种宏大的错觉,一种如此深刻且成功的精妙戏法,以至于我们在使用设备时每分每秒都将其视为理所当然。我们看到一台计算机同时运行着网页浏览器、音乐播放器和文字处理器,每个程序都在自己的窗口中,每个程序似乎都独占着机器的全部注意力。但是,一个中央处理器(CPU)——一个单独的大脑——如何能在同一时刻思考多个问题呢?简单的答案是:它不能。它所做的,是远比这更聪明的事情:它在进行“腾挪调度”。
想象一位国际象棋特级大师同时与二十位对手对弈。她没有二十个大脑,只有一个。她轻快地从一个棋盘走到另一个棋盘,在每个棋盘上走一步,然后迅速移到下一个。对于任何一个对手来说,她似乎都在连续不断地与他们下棋,尽管中间有停顿。但从鸟瞰的角度,我们看到她在同一时期内在所有二十盘棋上都取得了进展。这种注意力的快速交错便是并发的精髓。
在单核计算机上运行的多任务操作系统就像这位特级大师。它让一个进程运行微乎其微的一瞬间,然后迅速切换到另一个,再切换到下一个,如此快速地循环,以至于它们看起来都在同时运行。这与并行有着本质的区别,并行就像是有二十位特级大师,每人负责一个棋盘,在同一瞬间同时走棋。并行是关于同时做很多事;并发是关于同时管理很多事。在一台只有一个处理核心的计算机上,不可能有真正的并行,但并发的力量创造了一种令人信服且极其有用的并行错觉。
这场错觉的大师,我们计算舞台的导演,是操作系统中一个名为调度程序的关键部分。调度程序的工作是决定在任何给定时刻哪个进程可以使用CPU,以及使用多长时间。历史上,有两种主要的哲学思想指导调度程序如何做出这一决策。
第一种是协作式多任务。这是一种“礼貌”模式。每个进程一直运行,直到它认为是一个合适的时机暂停,并自愿将CPU的控制权交还给操作系统,这个动作称为让步 (yielding)。如果所有程序都是行为良好的“公民”,这种方式会运作得非常漂亮。但如果有一个程序不守规矩呢?想象一个陷入无限循环的错误程序,或者一个干脆拒绝让步的恶意程序。这样的程序会永远霸占CPU,整个系统将陷入停顿。你的鼠标会冻结,键盘会没有响应——宏大的错觉将会破灭。
这种脆弱性导致了当今主流方法的兴起:抢占式多任务。在这种模式下,操作系统不相信程序会彬彬有礼。它使用一个硬件闹钟,即计时器中断,来保持控制权。调度程序给一个进程一个时间片,称为时间量 (time quantum)(或时间片),可能只有几毫秒长。当闹钟响起时,无论正在运行的进程在做什么,操作系统都会强行停止(抢占)它,并切换到队列中的下一个进程。这保证了没有单个进程可以劫持系统,并确保即使是交互式程序也有机会运行,从而保持系统的响应能力。
当然,这也引入了一个关键的权衡。一个非常短的时间量使系统感觉响应极快,因为每个程序都能非常频繁地获得关注。然而,在进程之间切换的行为是有成本的。如果时间量太短,操作系统花费在切换“演员”上的时间比让他们“表演”的时间还多,系统的整体效率就会下降 [@problem_gpid:3664916]。
“切换”进程意味着什么?它不仅仅是跳转到程序的另一部分。每个进程都有其自身的上下文——它的程序计数器(当前执行到哪条指令)、CPU寄存器中的值等等。上下文切换就是操作系统保存即将停止的进程的整个上下文,并加载即将开始的进程的上下文的行为。
这个保存和恢复状态的过程需要时间。但还有一个更微妙且显著的成本。现代CPU就像装配线,使用一种称为流水线 (pipelining) 的技术来同时处理多条指令,每条指令处于不同的执行阶段。一次上下文切换会粗暴地打断这条装配线。流水线必须被清空,丢弃所有部分完成的工作。这会引入个浪费的周期,或称“气泡 (bubbles)”,在这些周期中没有完成任何有用的工作。
我们可以为此建立一个简单而强大的模型。如果我们执行的每条指令都有一个微小的概率触发上下文切换(由于计时器中断或其他操作系统事件),并且每次切换的成本是个周期,那么执行一条指令的平均周期数就不再是一。有效的每周期指令数 (IPC),一个衡量吞吐量的指标,变为:
这个优美的小公式揭示了抢占的基本“税收”。随着上下文切换频率 () 的增加或切换代价 () 的增大,系统的整体性能会下降。多任务处理并非免费;它是在响应能力和原始吞吐量之间的一种权衡,一种由操作系统不断管理的平衡。
让每个进程轮流使用CPU只是故事的一半。要真正有用,多任务处理必须提供隔离。你的音乐播放器中的一个错误不应该能够使你的文字处理器崩溃,更糟糕的是,不应该使整个操作系统崩溃。每个进程必须生活在自己的私有宇宙中,与其他所有进程隔离开来。这不仅仅是一个软件技巧;它需要CPU硬件本身的深度配合。
这种隔离的基础建立在两个硬件概念之上:
特权级别: CPU至少有两种操作模式:一种是受限的用户模式,用于应用程序;另一种是拥有全部权限的监管模式(或内核模式),用于操作系统。关键指令,如那些可以停止机器或直接访问硬件的指令,是特权的,只能在监管模式下执行。如果一个用户模式的应用程序试图做一些被禁止的事情,它会触发一个陷阱 (trap),将控制权交给操作系统,然后操作系统可以安全地处理这个越界行为。当一个程序需要合法的操作系统服务时,比如读取一个文件,它会进行一次系统调用,这是一个正式、受控的转换,进入监管模式,让内核代为执行任务。
虚拟内存: 这也许是所有技巧中最优雅的一个。操作系统和CPU的内存管理单元 (MMU) 协同工作,为每个进程提供其自己的、私有的、连续的地址空间。当进程A访问内存地址 0x4000 时,MMU会将该虚拟地址转换为计算机RAM中的一个物理位置。当进程B访问相同的虚拟地址 0x4000 时,MMU会将其转换为一个完全不同的物理位置。这就像两个人住在门牌号同为“主街101号”的房子里,但却在完全不同的城市。他们永远不会意外地走进对方的家。
这提供了强大的内存保护,但难道不显得浪费吗?如果我们启动八个相同的网页浏览器实例,是否需要在物理内存中保存八份相同的代码副本?答案是否定的,这要归功于一种名为写时复制 (Copy-on-Write, COW) 的巧妙优化。最初,操作系统将所有八个进程的虚拟内存映射到包含浏览器代码的相同物理RAM页面上。它们和平地共享。只有当某个进程试图修改那段代码时(这是一个罕见事件),操作系统才会介入,为该进程制作一份被修改页面的私有副本,并更新其内存映射。这是“共享直至必须分离”的原则,是效率与安全的完美结合。
虽然进程生活在各自私有的内存宇宙中,但它们最终必须与共享的、真实的世界互动。它们竞争有限的资源,如打印机、文件或网络连接。这种竞争被称为资源争用 (resource contention)。形式上,我们可以说,当“存在一个资源,同时存在两个不同的进程和,使得在等待并且也在等待”时,就存在争用。操作系统必须充当裁判,通常使用锁之类的机制来确保一次只有一个进程使用一个资源。
但这种加锁机制引入了一种新的、潜在的危险:死锁 (deadlock)。想象两个进程和,以及两个资源和。
现在它们陷入了“死亡拥抱”。在等待,在等待。两者都无法继续,除非操作系统干预,否则它们将永远等待下去。这种循环依赖可以通过打破其必要条件之一来防止。两种优雅的策略是:
防止“持有并等待”: 强制规定一个进程在请求另一个资源时不能持有任何资源。它必须在请求新锁之前释放它持有的所有锁。这很简单,但可能效率低下。
强制锁排序: 一种更复杂的方法是为系统中的每个锁分配一个唯一的等级或编号。然后,强制执行一个全局规则:所有进程必须严格按照锁的等级递增顺序获取锁。这样,循环等待就变得不可能了,因为要形成一个循环——等待,...,等待——锁的等级必须是严格递增的,但链条中的最后一环将要求在高等级锁之前获取一个低等级的锁,这违反了规则。这种简单的、有纪律的排序神奇地消除了死锁的可能性。
由虚拟内存提供的隔离之墙似乎坚不可摧。进程A无法读取进程B的内存。句号。但这种隔离是完美的吗?真相更为微妙。虽然操作系统隔离了架构状态(属于程序员模型的寄存器和内存),但它没有也无法完全隔离底层的微架构状态。
例如,高速缓存 (Caches) 是一种微架构优化。它们不属于程序员的抽象机器的一部分,但它们是物理硬件,由在同一核心上运行的进程分时共享。想象一个恶意进程Eve与一个受害进程Bob在同一核心上运行。Eve无法读取Bob的数据。但是,如果Eve可以执行一条清空CPU缓存的指令呢?在Bob运行片刻并用其数据填满缓存后,调度程序切换到Eve。Eve清空缓存。当调度程序切换回Bob时,他的程序突然运行得慢了很多,因为他所有的数据都已从缓存中被驱逐,必须从主内存中重新获取。Eve看不到Bob的数据,但她可以通过干扰共享的缓存状态来观察他操作的时间。这就是时间侧信道攻击的基础,它深刻地提醒我们,我们简洁的软件抽象是建立在混乱的物理硬件之上的,有时,物理特性会泄漏出来。
我们的旅程始于单核CPU上多任务处理的错觉。但现代计算机是真正的并行机器,拥有多个核心。这将调度程序的角色从一个简单的时间分片器提升为一个复杂的资源分配器。调度程序现在不仅要决定一个进程何时运行,还要决定它在何处以及在多少个核心上运行。
考虑一个必须同时运行一个延迟敏感的视频会议(需要在毫秒内响应)和一个大规模科学计算(需要最大吞吐量)的系统。调度程序必须进行精妙的平衡。它计算出满足视频会议延迟目标所需的最小核心数。它为该任务专门保留这个核心。剩下的个核心则被分配给科学计算,从而确保关键的截止期限得以满足,同时最大限度地利用机器进行批量工作。
这就是最前沿的技术:一种跨越时间和空间的动态、面向目标的计算分布。从快速切换任务的简单技巧开始,我们已经达到了一个复杂而优美的交响乐,其中操作系统与硬件协同,将数十或数百个计算线程编排成一个连贯、强大且响应迅速的整体。错觉已经变成了宏伟的现实。
在探索了多任务处理的原理——即计算机同时处理多个任务的巧妙方法——之后,我们可能会认为这是一个已经解决的问题,是我们设备内部安静运行的精巧工程。但这样做就像只欣赏一笔笔触而错过了整幅画。当我们不把它看作一个独立的概念,而是看作贯穿现代科学技术结构的一条基本线索时,多任务处理的真正美妙之处才会展现出来。它是一场宏大芭蕾舞中无形的编舞者,处理器、内存、网络乃至物理材料都在其指挥下和谐地舞动。让我们踏上一段旅程,看看这一个思想如何在从纯粹的编程逻辑到硬件的硬核物理学等众多领域中绽放。
在计算机能够进行多任务处理之前,程序员必须先构想出它。我们如何能仅使用编程语言的基本逻辑,创造出两个或更多进程交错运行的错觉?答案是一段美妙的计算机科学理论,感觉就像一个魔术。事实证明,暂停一个计算并恢复另一个计算的能力,可以使用一种称为续体 (continuation) 的概念从头构建——续体是一个代表“计算的其余部分”的对象。
想象一个生成无穷数字序列的生成器函数。它不是永远运行下去,而是生成一个数字然后“让出 (yields)”。为此,它将生成下一个数字所需的一切打包成一个续体,并将当前数字和这个续体一并返回给调用者。调用者可以使用这个数字,并在准备好时,调用该续体来获取下一个数字。这种“舞蹈”可以通过函数式编程中的一种名为相互尾递归的技术优雅地实现,即两个函数在其执行的最后相互调用。在一种能正确优化这些“尾调用 (tail calls)”的语言中,这种来回的舞蹈可以永远持续下去,而不会加深调用栈,从而避免因栈溢出而崩溃。这揭示了一个深刻的真理:看似复杂的协作式多任务机制,其核心是能够将计算本身视为可以被传递和随意调用的一等对象 (first-class object) 的直接结果。
当然,这种优雅并非没有代价。在现实世界中,处理器执行的每条指令都需要时间。当我们将一个协作式任务——比如一个每次迭代就自愿让出控制权的循环——转换成机器的本地语言时,让出的行为不是免费的。处理器必须保存其当前状态(如循环计数器),并调用调度程序,然后由调度程序决定接下来运行什么。这种簿记工作 (bookkeeping) 会带来虽小但可观的开销。分析总指令数揭示了一个权衡:让出过于频繁会使系统因调度开销而陷入困境,而让出过于不频繁则会使系统无响应。系统的性能成为这个让出频率的函数,这是高级软件设计选择与CPU周期的冰冷现实之间的直接联系。
一旦我们有了多个任务和多个工作者——比如现代处理器中的多个核心——一个新的核心挑战就出现了:我们如何让每个人都保持忙碌?如果一个核心工作量饱和而其他核心闲置,我们的并行机器就不比单核机器好。这就是负载均衡的艺术,一个充满微妙权衡的问题。
广义上说,存在两种哲学。在静态分区方案中,我们预先划分工作,并为每个工作者分配固定的部分。这很简单,开销很小,就像在游戏开始时给玩家发牌一样。或者,在动态调度方案中,我们可以使用一个中央任务队列。每当一个工作者空闲下来,它就去队列中取下一个任务。这种方式适应性更强,因为一个完成几个快速任务的工作者可以回来取更多任务,而一个被长任务卡住的工作者也不会耽误其他人。
哪种更好?这完全取决于工作的性质。如果已知所有任务耗时相同,那么简单的静态方法效率极高。但如果任务持续时间不可预测且变化很大——就像在现实世界中经常发生的那样——动态队列就显得优越得多,因为它自然地平滑了这些变化,并防止了工作者的闲置。
同样的张力也出现在像MapReduce这样的大规模数据处理系统中。在一个常见的设置中,一个“主”处理器为大量的“工作”处理器调度任务。主处理器本身可能成为瓶颈。如果我们将工作分解成太多微小的任务,主处理器将把所有时间都花在调度上,而工作者则在等待。如果我们创建的任务太大太少,我们又会失去并行的好处。介于两者之间存在一个“最佳点 (sweet spot)”——一个最优的任务数量,它完美地平衡了集中式调度的开销与并行处理的收益。这个最优值通常可以通过分析推导出来,揭示了对立力量之间的美妙平衡。
为了摆脱中央调度器的瓶颈,最优雅的系统使用一种名为工作窃取 (work-stealing) 的去中心化策略。在这里,每个工作者都有自己的小任务队列。如果一个工作者任务耗尽,它不会只是闲置;它会主动从另一个更忙碌的工作者的队列中“窃取”一个任务。这种方法具有极好的鲁棒性和可扩展性。然而,天下没有免费的午餐。窃取的行为涉及核心之间的同步和通信,这会带来开销。性能的关键是确保任务足够大,值得去窃取。存在一个“盈亏平衡粒度 (break-even granularity)”,即一个最小的任务大小,此时并行执行它的好处超过了窃取它的成本。这种精确的平衡行为是许多现代高性能计算框架的核心。
在现代计算机中,CPU不是唯一的表演者。它更像是一个乐团的指挥,乐团成员包括图形处理单元(GPU)、网卡和存储设备。真正掌握多任务处理,需要编排整个乐团,确保不同的硬件组件协同工作,以隐藏延迟并最大化吞吐量。
考虑一个大规模的科学模拟,比如在超级计算机上模拟流体动力学。一个常见的策略是使用强大的GPU进行繁重的数值计算。计算被分为“内部”部分和“边界”部分。当GPU忙于计算模拟广阔内部的下一个状态时——这个任务可能需要几毫秒——CPU可以协调将边界数据通信到相邻的计算机。通过使用异步、非阻塞调用,网卡可以在GPU进行计算的同时传输这些数据。如果做得正确,整个通信时间可以被“隐藏”在计算时间之后。一个步骤的总时间不是计算和通信时间之和,而是两者的最大值。这种重叠是跨异构硬件的一种多任务形式,是将科学应用扩展到巨大规模的关键。
最小化开销的原则在高性能网络中也至关重要。当服务器被网络数据包淹没时,最明显的方法——让网卡为每个到达的数据包中断CPU——是灾难性的。每次中断都会强制进行上下文切换,保存当前任务并运行网络处理程序。在高负载下,CPU将把所有时间都花在切换上下文上,这种现象称为“活锁 (livelock)”,没有时间留给有用的工作。一种更聪明的策略,常用于称为“单核操作系统 (unikernels)”的专用操作系统中,是关闭中断,让应用程序在一个紧密循环中轮询网卡。它不是逐个抓取数据包,而是大批量地抓取。通过在单个用户级上下文中处理整批数据包,所有上下文切换的开销都被消除了。这用等待轮询的延迟换取了吞吐量的巨大提升,展示了I/O和调度策略的选择如何能对性能产生千倍的影响。
对于大多数应用来说,平均速度快就足够了。但对某些应用而言,并非如此。在汽车的刹车系统、飞机的飞行控制器或工厂机器人中,一个错过截止日期的任务不仅仅是慢了——它是一次灾难性的失败。这就是实时系统的领域,在这里,可预测性至高无上。
在这个世界里,抢占式和协作式多任务之间的选择具有生死攸关的后果。想象一个系统,其中一个高优先级任务(例如,“踩下刹车”)必须每10毫秒运行一次,但一个低优先级任务(例如,“更新仪表盘显示”)当前正在执行。在一个完全抢占式的系统中,操作系统会立即暂停显示任务并运行刹车任务。但在一个协作式系统中,我们必须等待显示任务自愿yield。如果yield点之间的代码耗时太长,刹车任务可能会启动晚了并错过其截止日期。像响应时间分析这样的严谨数学技术允许工程师计算出低优先级任务在不让出的情况下可以运行的绝对最大时间,即一个值,以保证所有截止日期在任何情况下都能得到满足。
即使在不那么关键的系统中,比如一个处理传感器数据的物联网(IoT)网关,不可预测的延迟也可能违反性能目标。在高负载下,多任务操作系统可能会耗尽物理内存,并诉诸于“交换 (swapping)”——将数据临时移动到慢得多的磁盘上。虽然这种情况很少发生,但每次交换事件都会引入巨大的延迟。利用排队论的数学,我们可以模型化即使是很小的交换概率(比如5%),也会如何显著增加平均消息延迟,可能将其推至可接受的阈值之外。这种分析将操作系统的内存管理策略与用户感知的整个系统的响应能力直接联系起来。
也许最惊人的联系在于操作系统的多任务行为与硬件本身的物理寿命之间。固态硬盘(SSD)不是一个无限的草稿纸;其内存单元在一定数量的编程/擦除周期后会物理磨损。SSD的控制器使用复杂的算法来管理这种磨损,但它深受来自操作系统的负载模式的影响。当操作系统以随机模式写入数据时,SSD的内部垃圾回收机制必须更加努力地工作,移动现有数据以腾出空间。这种额外的内部I/O被称为写入放大 (Write Amplification, WAF)。例如,2.26的WAF意味着操作系统每写入100GB数据,实际上有226GB数据被写入到物理闪存单元中。
多任务操作系统,凭借其文件系统布局和I/O调度,决定了写入模式的随机性。它还决定是否使用TRIM命令通知驱动器有关已删除的文件,这可以显著减少写入放大。因此,操作系统的高级决策对WAF有直接的、可量化的影响,从而影响驱动器单元的消耗速度。通过分析操作系统的负载,我们可以预测驱动器健康状况(由其SMART指标报告)的衰减率,并预测其剩余的运行寿命。在这里,操作系统任务和调度器的抽象世界伸出手触及了物理世界,决定了它所运行的硅片的寿命。
从续体的逻辑纯粹性到存储设备的物理退化,多任务处理的原理是一股统一的力量。它是并发与开销、响应能力与吞吐量、软件的抽象世界与物理的无情法则之间不断的协商。正是这种无形但无处不在的艺术,使我们的数字世界不仅成为可能,而且高效、响应迅速且可靠。