2.4.1 DL调度器 - 调度算法

Deadline调度器的核心思想是EDF(Earliest Deadline First)与CBS(Constant Bandwidth Server), 我们将在本节详细讨论。

Deadline任务有三个重要的属性:runtime, period, deadline, 调度器需要确保任务在每个period时间窗口内得到runtime这么多的CPU时间,并且必须在deadline这个时间点之前得到。我们可以将一个Deadline任务看着是一个连续的子任务流,而period实质上代表着子任务的激活模式。例如一个视频处理引擎每秒需要处理60帧数据,每个数据帧的处理时间最长为10ms,并且新的数据帧每16ms就会到达,因此该进程的period是16ms, runtime是10ms, 由于这里没有显示的指定每帧数据处理的deadline, 因此默认与period相同。

这三个属性在用户启动Deadline任务时就需要指定,例如:

chrt -d --sched-runtime 5000000 --sched-deadline 10000000 \
    --sched-period 16666666 0 video_processing_tool

在该命令中,三个参数的单位都是纳秒,其中参数 0 用来表示进程优先级,由于Deadline 任务的优先级是固定的-1, 所以该值在这里没有实际意义,仅用来充当占位符的作用。

接下来我们讨论一下系统如何对Deadline任务进行调度。首先来看最简单的情况:系统只有一个任务,那么调度器在每次任务激活的时候直接调用即可,如下图所示:

但如果有多个任务的话,调度器就需要合理地安排每个任务的执行时间,确保每个任务在每个period内都按时完成。Linux采用的调度算法叫EDF(Earliest Deadline First), 即在任意时刻,调度器都挑选Deadline最近的任务执行。下图展现了三个任务的调度示例:

图中三个任务的runtime与period都不一样,此时将产生一个问题:当有多个任务同时存在时,如何确保所有任务在理论上确实是能够按时完成的呢?系统通过CPU带宽(Bandwidth)来进行判断,一个任务需要的CPU带宽的计算方式如下:B = runtime/period, 如果系统中所有Deadline任务的带宽之和不超过1, 那么理论上调度器就可以按时完成所有的任务。直白一点说,就是如果在时间窗口T内,所有任务对时间的需求总量小于T, 那么这些任务就可以按时完成,这是很自然的逻辑。例如上图中,各个任务的参数为:

TaskRuntimePeriod

T1

1

4

T2

2

6

T3

3

8

三个任务消耗的CPU带宽为:U = 1/4 + 2/6 + 3/8 = 23/24, 因此系统在理论上可以完成调度。

为了保证所有的Deadline任务能够按时完成,系统有一个准入机制,前面提到系统在启动Deadline任务时必须指定runtime, deadline与period, 系统会根据这些参数对任务的带宽需求进行校验,如果已经没有足够的剩余带宽来负担新任务,那么该任务就会被拒绝。

到此为止,调度器能够正确地工作还基于一个隐式的假设:即在每个period内,每个任务的运行时间都不会超过自己的runtime时长。如果某个任务的执行时间超出了自己的runtime的话,就可能会对其他任务造成影响,甚至造成雪崩效应,导致所有任务的执行时间都超过deadline, 无法按时完成。 这是调度器必须解决的问题,runtime的设置取决于用户对每个子任务耗时的预估,最大值可以设置为子任务的WCET(Worst-case Execution Time),但这样仍然存在两个问题:

  1. 造成带宽浪费,降低调度器的吞吐量 因为只有在少数情况下,子任务的处理时间才会达到WCET, 其他绝大多数情况下都不需要这么多时间,所以如果所有任务都按照WCET设置runtime的话,事实上是给每个任务预留了过多的CPU带宽,这会导致系统的准入机制拒绝更多的任务进入系统。

  2. WCET也不可靠 即使将每个任务的runtime都设置为WCET, 系统也有可能在某些极端情况下导致某个任务在某次处理中超出了runtime. runtime不够实际上并不是这个问题的核心,问题的核心是我们需要某种机制来对任务的行为进行隔离:如果一个任务的runtime设置过小,那么它应该只影响自己,其它“守信”的任务不应该受到影响。

Linux采用CBS来解决这个问题,CBS 实际上是一种带宽预留机制,其原理如下:

  1. 每个任务通过 scheduling deadlineremaining runtime 来描述当前状态,前者表示当前子任务的deadline, 后者表示任务在 scheduling deadline 之前还剩多少runtime可以用

  2. 当任务被唤醒时,系统会对该任务做如下两方面的校验:

    1. scheduling deadline < now, 即查看该任务是否已经错过了当前deadline

    2. remaining runtime / (scheduling deadline - now) > runtime / period, 校验带宽是否溢出,即在当前period中,CPU是否已经无法满足该任务的带宽要求

    如果二者都不成立,则说明该任务状态良好,否则则说明该任务已经无法在当前period内按时完成了,需要将其往后顺移,调度器会对其状态做如下修改: scheduling deadline = now + deadline remaining runtime = runtime

  3. 任务的运行时间会从 remaining runtime 中及时减去

  4. remaining runtime <= 0 时,说明该任务已经没法在deadline前交差了,此时该任务的状态会被设置为throttled, 调度器将暂停对其进行调度。调度器会在任务的 scheduling deadline 这个时间点重新为其设置状态,这个动作叫着replenishment, 该时间点被称为 replenishment time

  5. 当时间运行到throttled任务的replenishment time时,调度器会通过如下方式更新任务的状态: scheduling deadline = scheduling deadline + period remaining runtime = remaining runtime + runtime

上述的规则细看起来比较绕,其实核心思想就是为给每个任务预留固定的runtime,如果有任务在自己的runtime内无法交差,那么就在下一个period中再为其分配时间。这样只要任务“老老实实”地遵守自己的参数设定去运行,调度器就能够确保为其分配足够的CPU时间,而如果有任务“不老实”,那么它也只会影响它自己。这样就完成了对任务的隔离,保证了系统的稳定性。

Last updated