try ai
科普
编辑
分享
反馈
  • 多处理器调度

多处理器调度

SciencePedia玻尔百科
核心要点
  • 现代多处理器调度倾向于使用每核运行队列而非单一全局队列,以克服可扩展性瓶颈,尽管这引入了负载均衡的需求。
  • 高效的调度是在相互冲突的目标之间不断寻求平衡的艺术,例如最大化系统吞吐量与维持缓存亲和性,或确保公平性与隔离性。
  • 为一组通用任务找到可证明最优调度方案的挑战是 NP-hard 问题,这意味着实际的调度器依赖于高效的启发式算法,而非完美的解决方案。
  • 调度的原则超越了操作系统,影响着编译器设计、虚拟化、实时系统,并从人工智能和数学优化中汲取解决方案。

引言

在每台现代计算机的核心,一场复杂的芭蕾舞正在持续上演:将任务分配给多个处理器核心。这个过程被称为多处理器调度,是释放并行硬件力量的关键。虽然它看似一个简单的分配工作的后勤问题,但实际上,这是一个充满迷人复杂性、优雅原则和不可避免权衡的领域。挑战不仅在于让所有核心保持忙碌,更在于智能地完成这项工作,驾驭由硬件架构、软件同步和计算的基本数学限制所塑造的环境。

本文深入探讨了定义多处理器调度的核心挑战和巧妙的解决方案。我们将首先探索支配操作系统如何做出调度决策的基本原则和机制。然后,我们将拓宽视野,看看这些相同的原则如何应用于广泛的跨学科联系和应用中,揭示调度作为计算领域一个普遍性问题。

原则与机制

想象一下,你是一家繁忙作坊的经理,拥有不止一个,而是许多相同的工作台,即我们的“处理器”或“核心”。一大长串的工作,即我们的“线程”或“任务”,正在等待处理。你的目标很简单:尽快完成所有工作。这,本质上,就是多处理器调度的挑战。它看似直截了当,但当我们层层剥茧,就会发现一个充满迷人复杂性、优雅原则和不可避免权衡的世界。这不仅仅是一个后勤问题;它是一场与计算机体系结构本身的共舞。

一个队列还是多个?第一个重大分歧

我们的第一个决策是组织性的。我们如何将工作分发给每个工作台的工人?我们可以设立一个单一的中央队列,每个工人都来这里领取下一个任务。或者,我们可以给每个工人分配他们自己的个人工作队列。在​​全局运行队列​​和​​每核运行队列​​之间的这种选择,代表了调度器设计道路上的一个根本性分岔路口。

单一队列方法,有时与​​非对称多处理 (AMP)​​ 相关联,其中一个主处理器可能负责处理调度,这种方法非常简单。下一个该做什么工作毫无疑问——就是队列最前面的那个。但当工作坊变得非常大,有很多很多工人时,会发生什么?他们都必须在同一个地方排队,争先恐后地查看列表并挑选工作。这就造成了一个瓶颈。协调访问这个单一队列的开销,通常由一个单一的软件“锁”来保护,会随着竞争者数量的增加而增长。实际上,一个合理的模型表明,这种竞争成本随处理器数量 PPP 线性增长。

另一种选择是我们在大多数现代​​对称多处理 (SMP)​​ 系统中看到的:每个核心都有自己的私有运行队列。这就像给每个工人一个自己的收件箱。它的可扩展性非常好。没有中央瓶颈。工人们可以直接从他们的本地队列中获取下一个工作。但这引入了一个新问题:如果一个工人的收件箱溢出了,而另一个却是空的,该怎么办?系统变得不平衡。为了解决这个问题,工人们需要定期相互沟通以重新分配工作。这种协调有其成本,但远比单一全局队列高效。它通常涉及一种分层通信模式,就像一个电话树,其开销仅随处理器数量呈对数增长,即 log⁡P\log PlogP。

因此我们面临一个权衡。让我们具体化一下。假设全局队列的开销成本(以处理器周期计)为 cAMP(P)=1000Pc_{\mathrm{AMP}}(P) = 1000 PcAMP​(P)=1000P,而每核队列系统的成本为 cSMP(P)=4000log⁡2Pc_{\mathrm{SMP}}(P) = 4000 \log_{2} PcSMP​(P)=4000log2​P。对于少量核心,全局队列的简单性胜出;其线性成本很小。但随着 PPP 的增长,SMP 方法的对数成本增长得非常非常慢。在某个点上,两条线会相交。对于这个特定模型,交叉点恰好发生在 P=16P=16P=16 个核心时。对于超过 16 个核心的系统,每核队列的分层均衡变得决定性地更有效率。这就是为什么几乎所有现代多核操作系统都走了每核队列的道路。

平衡的艺术:在核心间腾挪工作

选择每核队列解决了一个问题,但又创造了另一个问题:保持工作负载平衡。一个空闲的核心是一种浪费的资源。调度的艺术现在变成了​​负载均衡​​的艺术——在核心之间腾挪任务以保持所有核心都忙碌。这可以通过将任务静态分配给核心(​​分区调度​​)或允许任务自由移动(​​全局调度​​)来实现。

这里存在另一个深刻的权衡:​​隔离性与效率​​。想象我们有两个核心。核心1被分配了两个任务,但其中一个意外地超出了其预期时间,要求比计划更多的工作。核心2的负载非常轻。在严格的分区方案下,核心1上的任务被困住了。它们无法从空闲的核心2获得帮助。结果是:核心1上错过了截止时间,尽管整个系统有足够的能力完成所有工作。过载被完美地隔离了,但系统是低效的。

一种允许任务迁移的全局调度方法可以解决这个问题。调度器会看到核心2的空闲时间,并将核心1的部分工作移过去,从而使所有工作都能按时完成。这带来了更高的整体利用率和吞吐量。但我们失去了完美的隔离性;一个核心上的问题现在通过消耗其他核心的资源来“干扰”它们。大多数现代系统寻求一种混合的、折中的方案:它们使用每核队列以提高效率,但会定期运行负载均衡算法来迁移任务,以防止严重的失衡。

这种迁移并非魔法;它是一个具体的决策。一个过载的核心应该向邻居​​推送(push)​​一个任务,还是一个负载不足的核心应该​​拉取(pull)​​一个任务过来?其逻辑由一个简单的成本效益分析驱动。将任务从繁忙核心 iii 迁移到较不繁忙核心 jjj 的好处是等待时间的减少。如果任务前面的工作队列在源核心上是 RiR_iRi​,在目标核心上是 RjR_jRj​,那么节省的等待时间是 Ri−RjR_i - R_jRi​−Rj​。但是迁移有成本 CCC,包括更新调度器数据结构等开销,以及最重要的一点,即因“冷”缓存造成的性能损失。一个理性的调度器只有在收益大于成本时才会迁移任务:即当 Ri−Rj>CR_i - R_j > CRi​−Rj​>C 时。这是一个极其简单而强大的原则。发起的时机也自然而然:一个过载的核心有动机去推送,一个空闲的核心有动机去拉取。

机器中的幽灵:缓存与内存亲和性

我们所说的迁移“成本”是什么?其中很大一部分是​​缓存亲和性的丧失​​。处理器的缓存是一个小型的、超高速的内存,用于存储最近使用的数据。当一个线程在一个核心上运行一段时间后,它的数据会填充缓存。这是一个“热”缓存,它使线程运行得快得多。当我们将线程迁移到另一个核心时,它的旧缓存被留在了后面,它必须在新核心的缓存中缓慢地重建其工作集。在这个“预热”期间,它会遭受更多缓慢的内存访问。

这种影响可能如此巨大,以至于一次看似有益的迁移实际上可能会损害性能。想象一下,将两个线程分配到两个核心上以平衡负载。这似乎应该使吞吐量比在单个核心上运行两者时翻倍。但是如果迁移成本,以因缓存未命中导致的额外停顿形式表现,足够高,完成两个任务的总时间实际上可能增加,导致整体系统吞吐量降低。调度器不能对硬件一无所知;它必须考虑到机器中的幽灵。

聪明的调度器利用这一点来为自己谋利。一个常见的策略是​​唤醒亲和性调度​​。当一个线程“醒来”(例如,因为它刚收到一些数据),调度器会尝试在唤醒它的线程正在运行的同一个核心上运行它。其逻辑是,它们很可能正在处理相关的数据,这些数据可能仍在该核心的缓存中。当然,这又引入了另一个权衡:遵循亲和性可能会带来更好的缓存性能,但如果许多线程都想在同一个核心上运行,也可能造成负载失衡。

这种“位置很重要”的原则超出了缓存的范畴。在大型、多核系统中,我们会遇到一种名为​​非统一内存访问 (NUMA)​​ 的架构。在这里,处理器访问连接到其自身插槽的内存要比访问连接到不同处理器插槽的内存快得多。机器不再是对称的;它有了地理。一个运行在远离其数据的核心上的线程可能会遭受显著的、持续的减速。调度器现在必须像一个地理学家一样,决定是让线程留在原地,为每次内存操作支付“远程访问税”,还是支付一次性的“迁移成本”将线程移回其“家”节点,在那里它可以全速运行。决策再次是一个简单的比较:一次性迁移成本 rir_iri​ 加上本地执行时间 wiw_iwi​ 是否小于减速后的远程执行时间 σi⋅wi\sigma_i \cdot w_iσi​⋅wi​?。

纠缠之舞:同步与群体动态

到目前为止,我们大多将任务想象成独立的短跑选手。但通常情况下,它们是一个团队的一部分,需要协同工作。这种协调通常通过锁(互斥锁)来完成,确保一次只有一个线程可以访问共享数据。这引入了一个臭名昭著的陷阱:​​优先级反转​​。

考虑一个双核系统。在核心1上,一个低优先级线程 L1L_1L1​ 获取了一个锁。在核心0上,一个高优先级线程 H1H_1H1​ 现在需要同一个锁。H1H_1H1​ 阻塞,等待 L1L_1L1​ 完成。但在核心1上,一个中等优先级的线程 M1M_1M1​ 变为就绪状态。由于 M1M_1M1​ 的优先级高于 L1L_1L1​,核心1上的调度器抢占了 L1L_1L1​ 并运行 M1M_1M1​。结果是灾难性的:高优先级线程 H1H_1H1​ 被卡住,等待低优先级线程 L1L_1L1​,而 L1L_1L1​ 又被中等优先级线程 M1M_1M1​ 阻止运行。

解决方案与问题本身一样优雅:​​优先级继承​​。当 H1H_1H1​ 因等待 L1L_1L1​ 持有的锁而阻塞时,系统会暂时将 L1L_1L1​ 的优先级提升到与 H1H_1H1​ 相等。现在,L1L_1L1​ 是核心1上优先级最高的线程,所以它立即运行,完成其在临界区的工作,并释放锁。一旦锁被释放,L1L_1L1​ 的优先级就恢复正常,H1H_1H1​ 就可以获取锁并继续执行。这个机制必须能够跨核心工作才能在现代系统中有效。

有时,协调甚至更紧密。想象一个线程流水线,其中一个线程的输出成为下一个线程的输入。这些线程必须同时运行才能取得进展。这需要​​组调度(gang scheduling)​​,其中操作系统调度器将整个组(“帮派”)视为一个单一实体,进行同步调度和抢占。对于这些应用程序来说,仅仅运行是不够的;它们必须一起运行。

现代万象与不可避免的不完美

随着​​异构多处理​​,即你智能手机中的“big.LITTLE”架构的出现,情节变得更加复杂。在这里,我们混合了强大的“大”核心和节能的“小”核心。显而易见的策略是在大核心上运行高优先级的、对性能要求高的任务。但是,分配给小核心的可怜的、低优先级的后台任务会怎么样呢?如果持续不断的高优先级工作流让大核心保持忙碌,甚至溢出到抢占小核心上的工作,我们的后台任务可能会​​饿死​​——可运行,但永远得不到运行。为了解决这个问题,调度器实现了公平性策略。一种常见的技术是​​老化(aging)​​:如果一个线程在可运行状态下等待时间过长(比如说,超过一个阈值 HHH),它的优先级会被暂时提升得足够高,以强制它在大核心上运行一段时间,仅仅为了确保它能取得进展。

在看到了所有这些巧妙的技巧和微妙的权衡之后,一个最终的、令人谦卑的问题出现了:凭借足够的聪明才智,我们能找到完美的调度方案吗?那个能为任何给定的作业集最小化总完成时间(makespan)的方案?

事实证明,答案几乎肯定是否定的。计算复杂性理论在这里给了我们一个深刻的见解。这个问题(称为 P∥Cmax⁡P \parallel C_{\max}P∥Cmax​)是​​NP-hard​​的,这意味着可能没有高效的算法来为一般情况找到最优解。但情况更糟。这个问题是​​APX-hard​​的,这意味着即使是找到一个保证接近最优的解决方案也很难。通过一个从另一个称为3-Partition的难题进行的精彩归约,我们可以证明,如果我们能够创建一个多项式时间算法,哪怕只是保证一个比某个界限好一点点的解,我们也就间接地解决了那个著名的难题。

这是一个深刻而美丽的真理。多处理器调度的局限性不仅仅是工程创造力的问题;它们被编织在计算本身的数学结构中。调度器的工作不是去实现一个不可能的完美,而是在这个复杂的权衡景观中航行——平衡负载与缓存亲和性,效率与隔离性,性能与公平性——使用聪明的、实用的启发式算法来创造一个在大多数情况下都能完美运行的系统。

应用与跨学科联系

窥探了调度器复杂的内部机制后,人们可能会倾向于将这些知识归档为操作系统设计师的专业课题。但这样做将只见树木,不见森林。调度的艺术和科学并不仅限于内核;它们是一个普遍问题的体现——在有限的时间内分配有限的资源——这个问题在广阔的科学和工程领域中回响。它是那只无形的手,指挥着从单个芯片内部的硅上芭蕾到预测我们天气的全球计算的一切。

现在让我们踏上一段旅程,看看这些思想将我们引向何方。我们将发现,调度的原则构成了一个十字路口,一个计算机体系结构、编译器设计、形式数学甚至人工智能在此交汇的繁华交叉点。我们的发现将揭示计算世界中一个惊人而美丽的统一性。

并行性能的物理学

想象一下,你有一个作坊,里面有一位大师傅和一大群学徒。你可以雇佣一千个,甚至一百万个学徒,但如果每一项任务都必须先由那位大师傅批准和分发,你很快就会发现,你的作坊产出并不会增加一百万倍。大师傅成了瓶颈。

这正是多处理器系统中的情况。“学徒”是渴望工作的处理器核心。“大师傅”通常就是操作系统调度器本身。如果调度器自己的代码必须在单个核心上运行来决定其他核心应该做什么,那么这部分串行工作就为我们能实现的总加速设置了一个硬性上限。这不仅仅是一种观点;它是并行计算的一个基本定律,一种关于性能的“物理学”,被称为 Amdahl 定律。它告诉我们,总性能最终受限于任务中无法并行的那部分。在一个极其直接的联系中,调度器思考所花费的时间 tst_sts​,成为了决定我们整个系统最终加速比的串行部分。无论我们增加多少核心,我们永远无法获得比总时间除以这个微小、顽固的串行部分更大的加速比。这迫使硬件和软件之间进行了一场引人入胜的对话:当架构师给我们更多核心时,操作系统设计者必须找到更巧妙的方法使调度器本身并行化,以免它成为其本应管理的瓶颈。

驯服复杂性的艺术:野外环境中的调度器

理论定律是优雅的,但现实世界是混乱的。一个真实的调度器不太像一个应用单一公式的物理学家,而更像一个在混乱十字路口指挥交通的杰出交警,不断地做出务实的权衡。

例如,考虑一个处于“启动风暴”(boot storm)中的系统——这相当于门一打开,一群狂热的人群冲过大门的数字版。几十个进程在同一瞬间都准备好运行。不同的调度策略如何应对?一个彩票调度器,它根据每个进程持有的“彩票”数量给予其赢得CPU的“机会”,以惊人的优雅处理了这种情况。一个持有大量彩票的高优先级交互式进程,在第一轮中被选中的概率极高。其概率性赋予了它稳健和可预测的响应能力。相比之下,一个步幅调度器,它是确定性的,在这种情况下可能很脆弱。在时间零点,所有进程都有相同的“通行”值,所以谁先运行的决定落在一个任意的平局打破规则上,比如进程ID。一个ID不佳的高优先级进程可能不得不等待许多其他进程先运行,仅仅因为它出生的怪癖。这揭示了算法设计的一个深刻真理:在长期内提供完美公平性的优雅确定性,有时会在关键时刻造成病态的“最坏情况”。

这种妥协的艺术在高性能计算(HPC)领域得到了充分展示,那里的调度器管理着运行大规模科学模拟的大陆级集群。这些系统经常运行巨大的、非抢占式的“批量作业”,可能需要数千个核心运行数小时。如果管理不善,较小的、较短的作业可能会被饿死,无休止地等待一个大作业完成。解决方案是一种优美的混合策略,称为*回填(backfilling)*。当一个大的批量作业到达但无法立即开始时,调度器会为它在未来做一个预留。然后,它查看调度中的“空洞”——预留前的一块空闲核心小时块——并巧妙地用那些保证在大作业需要开始前完成的小作业来“回填”它。这就像一场用计算资源玩的俄罗斯方块游戏,极大地提高了系统利用率和吞吐量,而不会不公平地惩罚小任务。这是实用工程学的证明,它将简单的调度原语变成了强大的、现实世界的系统。

超越速度:当正确性就是一切

到目前为止,我们一直在谈论性能和公平性方面的调度。但是有一个领域,这些考量是次要的。在一个实时系统中——控制电传飞控飞机、医疗起搏器或防抱死制动系统的计算机——一个完成晚了的任务不仅仅是慢了;它是错了。

在这里,调度器的工作不是为平均情况优化,而是为最坏情况提供保证。这引出了一个完全不同的研究领域。在单处理器上,规则是众所周知的;像最早截止期优先(EDF)这样的算法是可证明最优的。但在多处理器系统上,我们的直觉可能会误导我们。人们可能认为,只要所有任务的总计算需求 ∑Ui\sum U_i∑Ui​ 小于可用核心数 ppp,一切就应该没问题。这是灾难性的错误。

由于所谓的“多处理器调度异常”,一组任务即使在系统负载很轻的情况下也可能错过它们的截止日期。想象一个具有短截止时间的高优先级任务变得可以运行,但所有核心当前都被片刻之前开始的低优先级任务占用。高优先级任务必须等待,而这小小的延迟可能导致一连串的截止日期错过。这迫使研究人员开发出一种更严格、更形式化的方法。他们设计了复杂的“可调度性测试”——数学公式,提供一个充分(虽然不总是必要)的条件来证明给定的任务集在给定的多处理器系统上将始终满足其截止日期。这将调度世界与形式化验证和安全关键工程联系起来,在这些领域,主要目标不是速度,而是可证明的正确性。

调度器的隐藏伙伴

操作系统是最显眼的调度器,但它并不孤单。将工作调度到处理器上的任务是如此基础,以至于它以多种形式出现,通常由隐藏的伙伴执行。

最重要的伙伴之一是​​编译器​​。当你在编程语言中写一个简单的循环,比如 for i in 1..n: A[i] = A[i-1] + B[i],它看起来是无可救药的串行。每个计算似乎都依赖于前一个。但是一个聪明的编译器可以识别出底层的数学结构。它看到执行的操作是加法,而加法是满足结合律的。这让它可以施展一个“魔术”。它将串行计算转换为一种高度并行的算法,称为前缀和(prefix-sum)或扫描(scan)。在这个算法中,工作被分配给所有的处理器核心。每个核心为其数据块计算一个局部和。然后,在一个协调的舞蹈中,核心们交换它们的局部结果,以“修正”它们的本地计算,使其与全局串行结果相匹配。这是微秒级别的调度,是在程序开始运行之前由编译器编排的一场交响乐,将串行描述转换为大规模并行执行。

另一个隐藏的伙伴是​​虚拟化层​​。在现代云计算和容器的世界里,应用程序在隔离的沙箱中运行。但是当其中一个容器化应用程序需要使用专用处理器,比如用于AI工作负载的图形处理单元(GPU)时,会发生什么?主机操作系统调度器通常没有“GPU”的概念;它的词汇量仅限于CPU和系统内存。解决方案是一种协作。容器运行时,作为操作系统的伙伴,将GPU的设备文件(它在 /dev 目录中的“地址”)在容器的隔离文件系统内变得可见。同时,它配置操作系统的cgroups机制——一个通用的资源控制工具——以授予容器与该特定设备通信的权限。然而,这个过程只授予访问权限;它不提供细粒度的调度。标准的操作系统工具无法,例如,限制一个容器只使用GPU内存的50%。这个限制推动了硬件供应商将调度能力直接构建到他们的芯片中,例如多实例GPU(MIG)等功能,可以将单个物理GPU划分为多个完全隔离的虚拟GPU,每个都可以分配给单个容器。这显示了在有效管理资源的追求中,硬件和软件之间不断演变的舞蹈。

宏大挑战:寻找“完美”调度

我们已经看到,调度是一个复杂而微妙的问题。这就引出了一个宏大的问题:我们能找到“完美”的调度吗?对于许多现实场景,可能的调度数量是天文数字,找到绝对最优的那个是一个计算上难以解决的(NP-hard)问题。面对这堵复杂性的墙,计算机科学家们转向其他学科寻求灵感。

从​​数学优化​​的世界,我们可以借用将调度构建为一个待解决的形式化问题的思想。如果问题有一个很好的结构——例如,将可分割的工作负载分配给不同的处理器以最小化总延迟——我们可以将其建模为一个*线性规划问题。我们定义决策变量(在GPU iii 上运行多少作业 jjj),约束条件(每个GPU的容量有限;每个作业有特定的需求),以及一个目标函数(最小化总时间,也许对违反服务水平协议的行为施加重罚)。然后我们可以将这个形式化描述输入到一个通用的、强大的求解器中,该求解器利用运筹学几十年的研究成果来找到可证明的最优分配。这是一种优美的、声明式的方法:我们陈述我们想要什么*,求解器会找出如何实现。

对于不适合这种清晰数学模型的更混乱的问题——例如,调度具有复杂优先约束的任务——我们转向​​人工智能​​和启发式搜索领域。我们不是试图计算出完美的解决方案,而是搜索一个非常好的方案。我们设计一种表示调度的方法,也许是作为一个简单的按优先级排序的任务列表。然后,我们编写一个解码器,它确定性地将这个优先级列表转换成一个完整的、尊重所有约束的调度。最后,我们定义一个“适应度函数”,给调度打分(例如,其总持续时间,或完工时间)。现在,舞台已经为AI算法——如“培育”更好优先级列表的遗传算法,或智能探索解空间的模拟退火——准备好了,让它在这个巨大的可能性景观中搜索具有非常高适应度的调度。我们可能找不到那个唯一的真最优解,但我们通常能找到一个异常好的解决方案,远优于简单的经验法则所能产生的。

一条统一的线索

我们的旅程即将结束。我们从操作系统内核内部开始,穿越了超级计算机的架构、编译器的逻辑、安全关键系统的严谨世界,以及数学和人工智能的抽象景观。

自始至终,调度问题一直是我们不变的伴侣。它是一条统一的线索,一个根本性的问题,迫使我们直面性能的极限、正确性的本质,以及优雅与实用主义之间的权衡。它提醒我们,在计算的宇宙中,如同在我们自己的宇宙中一样,最深刻的挑战不仅仅是拥有资源,而是明智地使用它们。