0%

操作系统调度

操作系统会同时处理多个请求,但是硬件的运行资源是有限的。调度就是用来协调每个请求对于资源的使用方法。

一般调度器会通过维护运行队列来管理任务。运行队列不一定需要使用先进先出的队列,例如在Linux中使用的调度器就会使用红黑树来运行队列。当任务在执行时出发一定的条件(例如运行了指定的时间片以后或者发起了IO请求),那么会被加入到运行队列之中,等待再次被调度执行。

调度指标

在计算机处理的任务之中,有一类任务被称为批处理任务(比如复杂的科学计算等等),这些任务在执行时无需与用户进行交互,其主要的调度指标为任务处理的吞吐量,需要让吞吐量尽可能高,周转周期(即花费的时间)尽可能短。

还有一类任务为交互式任务,,需要在执行过程中对用户的操作做出响应。这类任务需要保证响应时间足够短使用户获得良好体验。

同时还有一类有截止时间要求的实时任务(例如车载操作系统需要检测汽车与外部物体的距离)。调度器必须让逝世人物在截止时间前完成,满足实时性的要求。

同时在移动设备中,调度器还需要尽可能地降低能耗。在通常情况下,调度器需要保证每个任务都有执行的机会,即满足公平性。调度器做出决策的时间也必须短,降低调度开销。

因此调度器在不同场景下有着不同的调度指标,需要在诸多方面做出权衡。

调度机制

进程可能处于不同的状态,这些状态包括新生、预备、运行、阻塞、终止。进程调度机制就负责在这些状态间进行转换。进程调度根据职责的不同,分为长期、中期、短期调度。

当用户像操作系统提交了执行某个程序的请求以后,系统可能不会立即处理该请求,这一决策由长期调度负责。长期调度用于限制系统中真正被短期调度管理的进程数量,避免短期调度的开销过大。只有当长期调度为某个程序创建了进程并且将状态设置为预备状态以后,才会由短期调度进一步管理该线程。

短期调度负责进程在预备状态、运行状态以及阻塞状态之间的转换。在短期调度决定执行某个进程以后,会将该进程从预备状态设置为运行状态。短期调度会使用适当的调度策略,尽可能地满足系统的调度指标。

中期调度实际上是换页机制的一部分。当系统中的进程已经占用了大量的内存资源以后,中期调度会挂起系统中被短期调度管理的进程,从而降低进程占用的内存总量。中期调度策略会根据策略来选择将要被挂起的进程,设置为对应的挂起状态,使其不再被调度执行。同时中期调度也会监控当前的内存使用情况,在适当的时机激活此前挂起的进程,使其可以重新被调度。

在引入了调度机制以后,进程转换的示意图变为如下所示。与之前的示意图相比,其区别在与引入了两个额外的进程状态:预备挂起状态以及挂起阻塞状态。

基于进程调度的进程转换

在进程调度中,长期中期短期相互协作,分别以不同的目标对进程进行调度。长期调度负责决定是否将一个新的进程纳入调度管理。中期调度负责限制系统中进程的内存占用。短期调度细粒度的调度进程的执行,做出对应的调度策略。

单核调度策略

经典调度

先到先得

先到先得策略也叫先进先出策略。该策略在系统中维护一个运行队列,在执行任务的时候,选取队列中的第一个任务,将其移除队列并且执行,当任务阻塞时,将其放入运行队列队尾,当一个任务执行完后,再执行下一个任务。

该策略的弊端在于在长短任务的场景下对于短任务不友好,会导致短任务的周转时间与运行时间之比过大。同时其对IO密集型任务也不够友好,可能会导致I/O密集型任务长时间内无法执行。

最短时间优先/最短完成时间任务优先

先到先得策略出现的问题提示我们需要让短任务立即进行执行。根据这一思想,得出的策略就是最短任务优先和最短完成时间任务优先。这两种策略的区别在于是否是抢占式的。前者必须等一个任务执行完成才能开始下一调度,后者会中断正在执行的进程转而执行其他任务。

这两种调度的弊端在于必须先预知任务的运行时间。同时前者调度策略其表现与任务到达时间点有着严重的依赖,后者调度策略会导致长任务饥饿。

时间片轮转

在时间片流转策略中需要为任务设置时间片,限定任务每次执行的时间。当任务执行完时间片以后,就切换到运行队列的下一个任务,这就是时间片轮转策略。由于时间片一般会设的足够小,所有任务都可以在一定时间内执行并且响应用户,从而将响应时间在可接受范围内。

该策略的弊端在于任务运行时间形似的场景下平均周转周期高,但是如果仅考虑响应时间,那么该策略会有很好的效果。

优先级调度

如果操作系统可以分别交互式任务以及批处理任务,那么可以设置让交互式任务优先执行的调度策略,为此调度器引入了优先级的概念。通过优先级概念,调度器可以确定哪些任务应该优先执行。

多级队列

每个任务都会被分配预先设置好的优先级,每个优先级一个队列,任务会被存储在对应的优先级队列之中。如果优先级不同的任务同时处于预备状态,那么调度器会选择优先级较高的任务进行调度。一个任务必须等到所有优先级比它高的任务调度完才可以被调度。

该策略会导致低优先级任务饥饿的问题,很容易出现因为大量的高优先级任务不断地进入系统导致低优先级任务饥饿。

多级反馈队列

在多级队列的基础上,多级反馈队列增加了动态设置任务优先级的策略。该策略会先对人物的运行时间进行评估,其中短任务会拥有更高的优先级。但是在真实世界中无法预知任务的完成时间,为此多级反馈队列策略会统计每个任务已经执行多长的时间,以此判断该任务是长任务还是短任务。当任务进入系统时,该策略会默认该任务为短任务,为其设置最高优先级。然后该策略会为每个任务队列设置任务的最大运行时间,如果任务在当前队列运行的总时间最终超过了队列允许的最大时间,那么会认为该任务是运行时间较长的任务,将该任务的优先级减一。

多级反馈队列策略能够动态的评估任务的运行时间,适配大致的任务优先级。同时在该策略中会定时的将所有任务的优先级提升至最高,保证不会出现任务饥饿的情况。该调度策略能够达到低平均周转周期的同时保证任务的响应时间,避免任务饥饿,在许多的操作系统中得到了应用,例如早期的Linux。

多核调度策略

相比于单核调度,多核调度策略还需要解决在哪个CPU上执行的问题。

负载分担

沿用单核跳读策略的思路,假设多核共享一个全局运行队列,当一个CPU核心需要调度任务时,从全局运行队列中获取一个任务作为下一个由他执行的任务。

该策略的优点在于设计实现简单,可以将多和调度策略问题规约为单核调度策略问题,同时不会出现CPU资源浪费的情况。但是也存在对应的问题,例如在多核处理器中,多核之间如何同步全局共享运行队列的信息或导致调度开销变得不可忽视。同时在多个CPU核心之间来回切换的开销很大,人物在不同CPU核心之间切换会导致诸如重新载入缓存、TLB刷新等问题。

协同调度

多线程为了充分利用多核处理器,通常会将工作量较大的任务切分为多个子任务,这些子任务中可能会存在依赖关系。协同调度的目的是尽可能让一组任务并行执行,避免调度器调度有依赖关系的两组任务。

群组调度是协同调度的一个经典策略,其基本思想是将关联任务设置为一组,并且以组为单位调度人物在多个CPU核心上执行,使他们的开始时间与结束时间尽可能保持接近。通过将任务以组为单位在多核处理器上进行调度,群组调度策略可以提升特定应用场景下的任务执行性能。

两级调度

由于全局调度器在不同CPU核心上切换执行会造成切换开销,为了减少开销,每个人物都尽可能只在一个CPU核心上进行调度。因此新的调度策略改为每个CPU核心都引入一个本地调度器,并用它管理对应核心上执行的任务。这种调度策略同时使用全局调度器和本地调度器,被称为两级调度。

当一个任务进入系统时,会根据系统的当前信息,决定该任务在哪个CPU核心上被执行。当一个任务被分配到给定的CPU核心时,将一直被该核心的本地调度器管理,不会迁移到其他CPU核心上运行。

负载均衡

两级调度将任务绑定在特定的CPU核心上调度执行,避免了任务在多核间切换,有着良好的性能。但是两级调度没有任务在CPU核心间切换的机制,会导致多核间的负载不均匀。为了解决这一问题,引入了负载均衡策略,通过追踪每个CPU核心的负载情况,将处于高CPU负载核心管理的任务迁移到低负载CPU核心上,保证每个核心负载大致相同。