截止期任务调度

0. 警告

修改这些设置可能会导致系统行为不可预测甚至不稳定。对于 -rt(组)调度,假设根用户知道他们在做什么。

1. 概述

sched_dl 调度类中包含的 SCHED_DEADLINE 策略基本上是 Earliest Deadline First (EDF) 调度算法的实现,并通过一种机制(称为 Constant Bandwidth Server, CBS)增强,使得任务之间行为的隔离成为可能。

2. 调度算法

2.1 主要算法

SCHED_DEADLINE [18] 使用三个参数,分别名为“运行时 (runtime)”、“周期 (period)”和“截止期 (deadline)”,来调度任务。一个 SCHED_DEADLINE 任务应在每个“周期 (period)”微秒内获得“运行时 (runtime)”微秒的执行时间,并且这些“运行时 (runtime)”微秒在周期开始后的“截止期 (deadline)”微秒内可用。为了实现这一行为,每当任务唤醒时,调度器都会计算一个与保证一致的“调度截止期 (scheduling deadline)”(使用 CBS[2,3] 算法)。然后使用 EDF[1] 基于这些调度截止期来调度任务(选择调度截止期最早的任务执行)。请注意,如果使用适当的“准入控制”策略(参见“4. 带宽管理”一节),任务实际上会在“截止期 (deadline)”内获得“运行时 (runtime)”单位的时间(显然,如果系统过载,此保证无法得到遵守)。

总而言之,CBS[2,3] 算法为任务分配调度截止期,使得每个任务在每个周期内最多运行其运行时,避免不同任务之间的任何干扰(带宽隔离),而 EDF[1] 算法选择调度截止期最早的任务作为下一个执行的任务。由于此功能,不严格遵守“传统”实时任务模型(参见第 3 节)的任务可以有效地使用新策略。

更详细地说,CBS 算法通过以下方式为任务分配调度截止期:

  • 每个 SCHED_DEADLINE 任务都由“运行时 (runtime)”、“截止期 (deadline)”和“周期 (period)”参数来表征;

  • 任务的状态由“调度截止期 (scheduling deadline)”和“剩余运行时 (remaining runtime)”描述。这两个参数最初都设置为 0;

  • 当 SCHED_DEADLINE 任务唤醒(准备好执行)时,调度器会检查是否

             remaining runtime                  runtime
    ----------------------------------    >    ---------
    scheduling deadline - current time           period
    

    然后,如果调度截止期小于当前时间,或者此条件得到验证,则调度截止期和剩余运行时将重新初始化为

    调度截止期 = 当前时间 + 截止期 剩余运行时 = 运行时

    否则,调度截止期和剩余运行时保持不变;

  • 当 SCHED_DEADLINE 任务执行时间 t 时,其剩余运行时减少为

    remaining runtime = remaining runtime - t
    

    (技术上,运行时在每个时钟周期或任务被取消调度/抢占时减少);

  • 当剩余运行时小于或等于 0 时,任务被称为“受限 (throttled)”(在实时文献中也称为“耗尽 (depleted)”),并且在其调度截止期之前无法被调度。此任务的“补充时间 (replenishment time)”(参见下一项)设置为等于调度截止期的当前值;

  • 当当前时间等于受限任务的补充时间时,调度截止期和剩余运行时将更新为

    scheduling deadline = scheduling deadline + period
    remaining runtime = remaining runtime + runtime
    

sched_attr 的 sched_flags 字段中的 SCHED_FLAG_DL_OVERRUN 标志允许任务通过发送 SIGXCPU 信号来了解运行时超限。

2.2 带宽回收

截止期任务的带宽回收基于 GRUB (Greedy Reclamation of Unused Bandwidth) 算法 [15, 16, 17],并在设置 SCHED_FLAG_RECLAIM 标志时启用。

下图说明了 GRUB 处理的任务的状态名称

                    ------------
        (d)        |   Active   |
     ------------->|            |
     |             | Contending |
     |              ------------
     |                A      |
 ----------           |      |
|          |          |      |
| Inactive |          |(b)   | (a)
|          |          |      |
 ----------           |      |
     A                |      V
     |              ------------
     |             |   Active   |
     --------------|     Non    |
        (c)        | Contending |
                    ------------

一个任务可以处于以下状态之一:

  • ActiveContending:如果它已准备好执行(或正在执行);

  • ActiveNonContending:如果它刚刚阻塞且尚未超过 0-lag 时间;

  • Inactive:如果它被阻塞且已超过 0-lag 时间。

状态转换

  1. 当一个任务阻塞时,它不会立即变为非活动状态,因为其带宽无法立即回收而不会破坏实时保证。因此,它进入一个称为 ActiveNonContending 的过渡状态。调度器启动“非活动定时器”,使其在 0-lag 时间触发,此时可以在不破坏实时保证的情况下回收任务的带宽。

    进入 ActiveNonContending 状态的任务的 0-lag 时间计算为

               (runtime * dl_period)
    deadline - ---------------------
                    dl_runtime
    

    其中 runtime 是剩余运行时,而 dl_runtime 和 dl_period 是保留参数。

  2. 如果任务在非活动定时器触发之前唤醒,则任务重新进入 ActiveContending 状态,并且“非活动定时器”被取消。此外,如果任务在不同的运行队列上唤醒,则任务的利用率必须从之前的运行队列的活动利用率中移除,并必须添加到新的运行队列的活动利用率中。为了避免任务在某个运行队列上唤醒而“非活动定时器”在不同 CPU 上运行时出现竞争,使用“dl_non_contending”标志来指示任务不在运行队列上但处于活动状态(因此,当任务阻塞时设置该标志,当“非活动定时器”触发或任务唤醒时清除该标志)。

  3. 当“非活动定时器”触发时,任务进入 Inactive 状态,其利用率将从运行队列的活动利用率中移除。

  4. 当非活动任务唤醒时,它进入 ActiveContending 状态,其利用率将添加到它所入队的运行队列的活动利用率中。

对于每个运行队列,GRUB 算法跟踪两种不同的带宽:

  • 活动带宽 (running_bw):这是所有处于活动状态(即 ActiveContending 或 ActiveNonContending)任务的带宽总和;

  • 总带宽 (this_bw):这是所有“属于”该运行队列的任务的总和,包括处于 Inactive 状态的任务。

  • 最大可用带宽 (max_bw):这是截止期任务可用的最大带宽,目前设置为 RT 容量。

该算法回收处于 Inactive 状态的任务的带宽。它通过以等于以下速度递减执行任务 Ti 的运行时来做到这一点:

dq = -(max{ Ui, (Umax - Uinact - Uextra) } / Umax) dt

其中

  • Ui 是任务 Ti 的带宽;

  • Umax 是最大可回收利用率(受 RT 节流限制);

  • Uinact 是(每个运行队列的)非活动利用率,计算为 (this_bq - running_bw);

  • Uextra 是(每个运行队列的)额外可回收利用率(受 RT 节流限制)。

现在来看一个简单的例子,两个截止期任务的运行时为 4,周期为 8(即带宽为 0.5)

       A            Task T1
       |
       |                               |
       |                               |
       |--------                       |----
       |       |                       V
       |---|---|---|---|---|---|---|---|--------->t
       0   1   2   3   4   5   6   7   8


       A            Task T2
       |
       |                               |
       |                               |
       |       ------------------------|
       |       |                       V
       |---|---|---|---|---|---|---|---|--------->t
       0   1   2   3   4   5   6   7   8


       A            running_bw
       |
     1 -----------------               ------
       |               |               |
    0.5-               -----------------
       |                               |
       |---|---|---|---|---|---|---|---|--------->t
       0   1   2   3   4   5   6   7   8


- Time t = 0:

  Both tasks are ready for execution and therefore in ActiveContending state.
  Suppose Task T1 is the first task to start execution.
  Since there are no inactive tasks, its runtime is decreased as dq = -1 dt.

- Time t = 2:

  Suppose that task T1 blocks
  Task T1 therefore enters the ActiveNonContending state. Since its remaining
  runtime is equal to 2, its 0-lag time is equal to t = 4.
  Task T2 start execution, with runtime still decreased as dq = -1 dt since
  there are no inactive tasks.

- Time t = 4:

  This is the 0-lag time for Task T1. Since it didn't woken up in the
  meantime, it enters the Inactive state. Its bandwidth is removed from
  running_bw.
  Task T2 continues its execution. However, its runtime is now decreased as
  dq = - 0.5 dt because Uinact = 0.5.
  Task T2 therefore reclaims the bandwidth unused by Task T1.

- Time t = 8:

  Task T1 wakes up. It enters the ActiveContending state again, and the
  running_bw is incremented.

2.3 节能调度

当选择 cpufreq 的 schedutil 调速器时,SCHED_DEADLINE 实现 GRUB-PA [19] 算法,将 CPU 运行频率降低到仍能满足截止期的最小值。此行为目前仅针对 ARM 架构实现。

如果改变频率所需的时间与保留周期处于同一数量级,则必须特别小心。在这种情况下,设置固定的 CPU 频率会导致更少的截止期错失。

3. 实时任务调度

警告

本节包含关于经典截止期调度理论的(不详尽的)摘要,以及它如何应用于 SCHED_DEADLINE。如果读者只对了解如何使用调度策略感兴趣,可以“安全地”跳到第 4 节。无论如何,我们强烈建议您回到这里继续阅读(一旦测试的冲动得到满足 :P),以确保完全理解所有技术细节。

对哪种任务可以利用这种新的调度机制没有限制,尽管必须说它特别适合需要其时间行为保证的周期性或偶发性实时任务,例如多媒体、流媒体、控制应用程序等。

3.1 定义

典型的实时任务由计算阶段(任务实例或作业)的重复组成,这些阶段以周期性或偶发性方式激活。每个作业 J_j(其中 J_j 是任务的第 j 个作业)的特征是到达时间 r_j(作业开始的时间)、完成作业所需的计算时间 c_j,以及作业的绝对截止期 d_j,这是作业应完成的时间。最大执行时间 max{c_j} 称为任务的“最坏情况执行时间”(WCET)。实时任务可以是周期性的,如果 r_{j+1} = r_j + P,则周期为 P;或者是偶发性的,如果 r_{j+1} >= r_j + P,则最小到达间隔时间为 P。最后,d_j = r_j + D,其中 D 是任务的相对截止期。总而言之,实时任务可以描述为

任务 = (WCET, D, P)

实时任务的利用率定义为其 WCET 与其周期(或最小到达间隔时间)之间的比率,表示执行任务所需的 CPU 时间的比例。

如果总利用率 U=sum(WCET_i/P_i) 大于 M(M 等于 CPU 数量),则调度器无法满足所有截止期。请注意,总利用率定义为系统中所有实时任务的利用率 WCET_i/P_i 之和。当考虑多个实时任务时,第 i 个任务的参数用“_i”后缀表示。此外,如果总利用率大于 M,则我们面临实时任务饿死非实时任务的风险。相反,如果总利用率小于 M,则非实时任务将不会被饿死,并且系统可能能够满足所有截止期。事实上,在这种情况下,可以为迟滞提供一个上限(定义为 0 和作业完成时间与其绝对截止期之间差值的最大值)。更精确地说,可以证明,使用全局 EDF 调度器,每个任务的最大迟滞小于或等于

((M − 1) · WCET_max − WCET_min)/(M − (M − 2) · U_max) + WCET_max

其中 WCET_max = max{WCET_i} 是最大 WCET,WCET_min=min{WCET_i} 是最小 WCET,U_max = max{WCET_i/P_i} 是最大利用率[12]。

3.2 单处理器系统可调度性分析

如果 M=1(单处理器系统),或在分区调度的情况下(每个实时任务静态分配给一个且只有一个 CPU),则可以形式化检查是否所有截止期都得到遵守。如果所有任务的 D_i = P_i,则当且仅当在 CPU 上运行的任务的总利用率小于或等于 1 时,EDF 才能遵守在 CPU 上运行的所有任务的所有截止期。如果某些任务的 D_i != P_i,则可以将任务的密度定义为 WCET_i/min{D_i,P_i},并且如果 CPU 上运行的任务的密度之和小于或等于 1,则 EDF 能够遵守在 CPU 上运行的所有任务的所有截止期

sum(WCET_i / min{D_i, P_i}) <= 1

需要注意的是,这个条件只是充分条件,而不是必要条件:存在可调度的任务集,但不符合该条件。例如,考虑由 Task_1=(50ms,50ms,100ms) 和 Task_2=(10ms,100ms,100ms) 组成的任务集 {Task_1,Task_2}。EDF 显然能够调度这两个任务而不会错过任何截止期(Task_1 一旦释放就立即调度,并及时完成以满足其截止期;Task_2 在 Task_1 之后立即调度,因此其响应时间不会大于 50ms + 10ms = 60ms),即使

50 / min{50,100} + 10 / min{100, 100} = 50 / 50 + 10 / 100 = 1.1

当然,可以测试 D_i != P_i 任务的精确可调度性(检查一个既充分又必要的条件),但这不能通过将总利用率或密度与一个常数进行比较来完成。相反,可以使用所谓的“处理器需求”方法,计算所有任务在大小为 t 的时间间隔内满足所有截止期所需的 CPU 总时间 h(t),并将此时间与间隔大小 t 进行比较。如果 h(t) 对于 t 的所有可能值都小于 t(即任务在大小为 t 的时间间隔内所需的时间小于该间隔的大小),则 EDF 能够调度任务并满足所有截止期。由于对 t 的所有可能值执行此检查是不可能的,因此已证明[4,5,6]仅需对 0 到最大值 L 之间的 t 值执行测试。引用的论文包含所有数学细节,并解释了如何计算 h(t) 和 L。无论如何,这种分析过于复杂且耗时,无法在线执行。因此,如第 4 节所述,Linux 使用基于任务利用率的准入测试。

3.3 多处理器系统可调度性分析

在具有全局 EDF 调度(非分区系统)的多处理器系统上,可调度性的充分测试不能基于利用率或密度:可以证明,即使 D_i = P_i 任务集,其利用率略大于 1,无论 CPU 数量如何,都可能错过截止期。

考虑一个在 M 个 CPU 系统上的 M+1 个任务的集合 {Task_1,...Task_{M+1}},其中第一个任务 Task_1=(P,P,P) 的周期、相对截止期和 WCET 等于 P。其余 M 个任务 Task_i=(e,P-1,P-1) 具有任意小的最坏情况执行时间(此处表示为“e”)和小于第一个任务的周期。因此,如果所有任务同时激活,全局 EDF 会首先调度这 M 个任务(因为它们的绝对截止期等于 t + P - 1,因此它们小于 Task_1 的绝对截止期,即 t + P)。结果是,Task_1 只能在时间 t + e 调度,并将在其绝对截止期之后的时间 t + e + P 完成。任务集的总利用率为 U = M · e / (P - 1) + P / P = M · e / (P - 1) + 1,对于较小的 e 值,这可能非常接近 1。这被称为“Dhall 效应”[7]。注意:Dhall 原始论文中的例子在此处略有简化(例如,Dhall 更正确地计算了 lim_{e->0}U)。

实时文献[8,9]中已经开发了更复杂的全局 EDF 可调度性测试,但它们不是基于总利用率(或密度)与固定常量的简单比较。如果所有任务都有 D_i = P_i,则可以简单地表达一个充分的可调度性条件:

sum(WCET_i / P_i) <= M - (M - 1) · U_max

其中 U_max = max{WCET_i / P_i}[10]。请注意,对于 U_max = 1,M - (M - 1) · U_max 变为 M - M + 1 = 1,此可调度性条件正好证实了 Dhall 效应。关于多处理器实时调度可调度性测试文献的更完整调查可在 [11] 中找到。

如前所述,强制总利用率小于 M 并不能保证全局 EDF 在不错过任何截止期的情况下调度任务(换句话说,全局 EDF 不是最优调度算法)。然而,总利用率小于 M 足以保证非实时任务不会被饿死,并且实时任务的迟滞具有上限[12](如前所述)。各种论文[13,14]中已经开发了关于实时任务最大迟滞的不同界限,但对于 SCHED_DEADLINE 而言,重要的理论结果是,如果总利用率小于或等于 M,则任务的响应时间是有限的。

3.4 与 SCHED_DEADLINE 参数的关系

最后,理解第 2 节中描述的 SCHED_DEADLINE 调度参数(运行时、截止期和周期)与本节中描述的实时任务参数(WCET、D、P)之间的关系非常重要。请注意,任务的时间约束由上述绝对截止期 d_j = r_j + D 表示,而 SCHED_DEADLINE 根据调度截止期(参见第 2 节)调度任务。如果使用准入测试来保证调度截止期得到遵守,那么 SCHED_DEADLINE 可用于调度实时任务,从而保证任务的所有作业截止期都得到遵守。为此,必须通过设置以下参数来调度任务:

  • 运行时 >= WCET

  • 截止期 = D

  • 周期 <= P

换句话说,如果 runtime >= WCET 并且 period <= P,那么调度截止期和绝对截止期 (d_j) 重合,因此适当的准入控制允许此任务的作业绝对截止期得到遵守(这被称为“硬可调度性属性”,并且是 [2] 中引理 1 的扩展)。请注意,如果 runtime > deadline,准入控制肯定会拒绝此任务,因为它无法满足其时间约束。

参考文献

1 - C. L. Liu 和 J. W. Layland. 多道程序设计的调度算法 -

在硬实时环境中。计算机械协会杂志,20(1),1973。

2 - L. Abeni, G. Buttazzo. 在硬实时系统中集成多媒体应用程序

实时系统。第 19 届 IEEE 实时系统研讨会论文集,1998 年。http://retis.sssup.it/~giorgio/paps/1998/rtss98-cbs.pdf

3 - L. Abeni. 多媒体应用程序的服务机制。ReTiS 实验室

技术报告。http://disi.unitn.it/~abeni/tr-98-01.pdf

4 - J. Y. Leung 和 M.L. Merril. 关于周期性实时任务抢占式调度的一个注释。

信息处理快报,第 11 卷,第 3 期,第 115-118 页,1980 年。

5 - S. K. Baruah, A. K. Mok 和 L. E. Rosier. 在单个处理器上抢占式调度

硬实时偶发任务。第 11 届 IEEE 实时系统研讨会论文集,1990 年。

6 - S. K. Baruah, L. E. Rosier 和 R. R. Howell. 关于在单个处理器上周期性实时任务抢占式调度的算法和复杂性。

实时系统杂志,第 4 卷,第 2 期,第 301-324 页,1990 年。

7 - S. J. Dhall 和 C. L. Liu. 关于实时调度问题。操作

研究,第 26 卷,第 1 期,第 127-140 页,1978 年。

8 - T. Baker. 多处理器 EDF 和截止期单调可调度性

分析。第 24 届 IEEE 实时系统研讨会论文集,2003 年。

9 - T. Baker. 多处理器上 EDF 可调度性分析。

IEEE 并行与分布式系统学报,第 16 卷,第 8 期,第 760-768 页,2005 年。

10 - J. Goossens, S. Funk 和 S. Baruah, 多处理器上周期任务系统优先级驱动调度。

实时系统杂志,第 25 卷,第 2–3 期,第 187–205 页,2003 年。

11 - R. Davis 和 A. Burns. 多处理器系统硬实时调度综述。

ACM 计算调查,第 43 卷,第 4 期,2011 年。http://www-users.cs.york.ac.uk/~robdavis/papers/MPSurveyv5.0.pdf

12 - U. C. Devi 和 J. H. Anderson. 全局 EDF 调度下多处理器的迟滞界限。

实时系统杂志,第 32 卷,第 2 期,第 133-189 页,2008 年。

13 - P. Valente 和 G. Lipari. EDF 在多处理器上调度软实时任务迟滞的上限。

第 26 届 IEEE 实时系统研讨会论文集,2005 年。

14 - J. Erickson, U. Devi 和 S. Baruah. 全局 EDF 改进的迟滞界限。

第 22 届 Euromicro 实时系统会议论文集,2010 年。

15 - G. Lipari, S. Baruah, 常量带宽服务器中未使用带宽的贪婪回收,

第 12 届 IEEE Euromicro 实时系统会议,2000 年。

16 - L. Abeni, J. Lelli, C. Scordino, L. Palopoli, SCHED DEADLINE 的贪婪 CPU 回收。

在德国杜塞尔多夫举行的实时 Linux 研讨会 (RTLWS) 论文集,2014 年。

17 - L. Abeni, G. Lipari, A. Parri, Y. Sun, 多核 CPU 回收:并行还是串行?

在 2016 年第 31 届 ACM 应用计算年度研讨会论文集。

18 - J. Lelli, C. Scordino, L. Abeni, D. Faggioli, Linux 内核中的截止期调度,

软件:实践与经验,46(6): 821-839,2016 年 6 月。

19 - C. Scordino, L. Abeni, J. Lelli, Linux 内核中节能实时调度,

第 33 届 ACM/SIGAPP 应用计算研讨会 (SAC 2018),法国波城,2018 年 4 月。

4. 带宽管理

如前所述,为了使 -deadline 调度有效和有用(即能够在“截止期”内提供“运行时”时间单位),拥有某种方法来控制 CPU 时间的可用部分分配给各种任务非常重要。这通常称为“准入控制”,如果未执行此操作,则无法保证 -deadline 任务的实际调度。

如第 3 节所述,正确调度一组实时任务所需的一个必要条件是总利用率小于 M。当谈到 -deadline 任务时,这意味着所有任务的运行时与周期之比的总和必须小于 M。请注意,运行时/周期之比等同于“传统”实时任务的利用率,也常被称为“带宽”。用于控制可分配给 -deadline 任务的 CPU 带宽的接口类似于已用于 -rt 任务的实时组调度(又称 RT-throttling - 参见 实时组调度)的接口,并且基于位于 procfs 中的可读/写控制文件(用于系统范围设置)。请注意,由于需要进一步讨论如何管理任务组级别的 SCHED_DEADLINE 带宽,因此尚未为 -deadline 任务定义每组设置(通过 cgroupfs 控制)。

截止期带宽管理与 RT 节流之间的主要区别在于,-deadline 任务拥有自己的带宽(而 -rt 任务没有!),因此我们不需要更高级别的节流机制来强制执行所需的带宽。换句话说,这意味着接口参数仅在准入控制时使用(即,当用户调用 sched_setattr() 时)。然后根据实际任务参数执行调度,以便将 CPU 带宽分配给 SCHED_DEADLINE 任务,以满足它们在粒度方面的需求。因此,使用这个简单的接口,我们可以限制 -deadline 任务的总利用率(即,Sum (runtime_i / period_i) < global_dl_utilization_cap)。

4.1 系统范围设置

系统范围设置在 /proc 虚拟文件系统下配置。

目前,-rt 旋钮用于 -deadline 准入控制,并且在 CONFIG_RT_GROUP_SCHED 启用时,-deadline 运行时会计入(根)-rt 运行时。当 !CONFIG_RT_GROUP_SCHED 时,该旋钮仅用于 -dl 准入控制。我们意识到这并非完全理想;但是,目前最好有一个小接口,以后可以轻松更改。理想情况(参见第 5 节)是从 -deadline 服务器运行 -rt 任务;在这种情况下,-rt 带宽是 dl_bw 的直接子集。

这意味着,对于包含 M 个 CPU 的 root_domain,可以创建 -deadline 任务,只要其带宽总和保持在以下值以下:

M * (sched_rt_runtime_us / sched_rt_period_us)

也可以禁用此带宽管理逻辑,从而可以自由地将系统超额订阅到任意级别。这可以通过在 /proc/sys/kernel/sched_rt_runtime_us 中写入 -1 来完成。

4.2 任务接口

指定一个周期性/偶发性任务,该任务在每个实例中执行给定量的运行时,并根据其自身时间约束的紧急程度进行调度,通常需要一种声明以下内容的方式:

  • 一个(最大/典型)实例执行时间,

  • 连续实例之间的最小间隔,

  • 每个实例必须完成的时间约束。

因此

  • 提供了一个新的 struct sched_attr,其中包含所有必需的字段;

  • 实现了操纵它的新调度相关系统调用,即 sched_setattr() 和 sched_getattr()。

出于调试目的,可以通过 /proc/<pid>/sched(dl.runtime 和 dl.deadline 条目,两个值均以纳秒为单位)检索 SCHED_DEADLINE 任务的剩余运行时和绝对截止期。目前正在讨论从生产代码中检索这些值的编程方式。

4.3 默认行为

SCHED_DEADLINE 带宽的默认值为 rt_runtime 等于 950000。在 rt_period 等于 1000000 的情况下,默认意味着 -deadline 任务在每个 root_domain 中最多可以使用 95% 的 CPU 时间,乘以构成该 root_domain 的 CPU 数量。这意味着非 -deadline 任务将获得至少 5% 的 CPU 时间,并且 -deadline 任务将以相对于“截止期”参数的保证最坏情况延迟获得其运行时。如果“截止期”=“周期”并且使用 cpuset 机制实现分区调度(参见第 5 节),那么这种简单的带宽管理设置能够确定性地保证 -deadline 任务将在一个周期内获得其运行时。

最后,请注意,为了不危及准入控制,-deadline 任务不能 fork。

4.4 sched_yield() 的行为

当 SCHED_DEADLINE 任务调用 sched_yield() 时,它会放弃其剩余的运行时并立即受到节流,直到下一个周期,此时其运行时将被补充(设置了一个特殊的标志 dl_yielded,用于在调用 sched_yield() 后正确处理节流和运行时补充)。

sched_yield() 的这种行为允许任务恰好在下一个周期开始时唤醒。此外,这在未来与带宽回收机制结合使用时可能会很有用,因为 sched_yield() 会将剩余运行时提供给其他 SCHED_DEADLINE 任务进行回收。

5. 任务 CPU 亲和性

-deadline 任务不能拥有小于其创建所在的整个 root_domain 的亲和性掩码。但是,可以通过 cpuset 设施 (CPUSETS) 指定亲和性。

5.1 SCHED_DEADLINE 和 cpusets HOWTO

以下是一个简单配置的示例(将一个 -deadline 任务固定到 CPU0)(rt-app 用于创建 -deadline 任务)

mkdir /dev/cpuset
mount -t cgroup -o cpuset cpuset /dev/cpuset
cd /dev/cpuset
mkdir cpu0
echo 0 > cpu0/cpuset.cpus
echo 0 > cpu0/cpuset.mems
echo 1 > cpuset.cpu_exclusive
echo 0 > cpuset.sched_load_balance
echo 1 > cpu0/cpuset.cpu_exclusive
echo 1 > cpu0/cpuset.mem_exclusive
echo $$ > cpu0/tasks
rt-app -t 100000:10000:d:0 -D5 # it is now actually superfluous to specify
                               # task affinity

6. 未来计划

仍缺少

  • 以编程方式检索当前运行时和绝对截止期的方法

  • 截止期继承的改进,特别是关于在不相互作用的任务之间保持带宽隔离的可能性。这正在从理论和实践两方面进行研究,希望能尽快提供一些演示代码;

  • 基于 (c)group 的带宽管理,以及可能还有调度;

  • 非根用户的访问控制(以及需要解决的相关安全问题),即允许非特权用户使用这些机制的最佳方式是什么,以及如何防止非根用户“欺骗”系统?

如前所述,我们还计划将这项工作与 EDF 节流补丁 [https://lore.kernel.org/r/cover.1266931410.git.fabio@helm.retis] 合并,但我们仍处于合并的初步阶段,我们非常希望获得有助于我们决定方向的反馈。

附录 A. 测试套件

SCHED_DEADLINE 策略可以使用两个应用程序轻松测试,它们是更广泛的 Linux 调度器验证套件的一部分。该套件可在 GitHub 仓库中找到:https://github.com/scheduler-tools

第一个测试应用程序名为 rt-app,可用于启动具有特定参数的多个线程。rt-app 支持 SCHED_{OTHER,FIFO,RR,DEADLINE} 调度策略及其相关参数(例如,niceness、priority、runtime/deadline/period)。rt-app 是一个有价值的工具,因为它可用于合成地重现某些工作负载(可能模仿实际用例),并评估调度器在此类工作负载下的行为。通过这种方式,结果易于重现。rt-app 可在以下地址获取:https://github.com/scheduler-tools/rt-app

线程参数可以从命令行指定,例如:

# rt-app -t 100000:10000:d -t 150000:20000:f:10 -D5

上述命令创建 2 个线程。第一个线程由 SCHED_DEADLINE 调度,每 100ms 执行 10ms。第二个线程以 SCHED_FIFO 优先级 10 调度,每 150ms 执行 20ms。测试将总共运行 5 秒。

更有趣的是,配置可以通过 json 文件描述,并作为输入传递给 rt-app,例如:

# rt-app my_config.json

用第二种方法可以指定的参数是命令行选项的超集。有关更多详细信息,请参阅 rt-app 文档(<rt-app-sources>/doc/*.json)。

第二个测试应用程序使用 chrt 完成,它支持 SCHED_DEADLINE。

用法很简单:

# chrt -d -T 10000000 -D 100000000 0 ./my_cpuhog_app

这样,my_cpuhog_app 将在 SCHED_DEADLINE 预留中运行,每 100ms 运行 10ms(请注意,参数以纳秒表示)。您还可以使用 chrt 为已运行的应用程序创建预留,前提是您知道其 pid

# chrt -d -T 10000000 -D 100000000 -p 0 my_app_pid

附录 B. 最小 main() 函数

下面提供了一个简单(简陋)的独立代码片段,展示了实时应用程序开发人员如何创建 SCHED_DEADLINE 预留:

#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <linux/unistd.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <sys/syscall.h>
#include <pthread.h>

#define gettid() syscall(__NR_gettid)

#define SCHED_DEADLINE       6

/* XXX use the proper syscall numbers */
#ifdef __x86_64__
#define __NR_sched_setattr           314
#define __NR_sched_getattr           315
#endif

#ifdef __i386__
#define __NR_sched_setattr           351
#define __NR_sched_getattr           352
#endif

#ifdef __arm__
#define __NR_sched_setattr           380
#define __NR_sched_getattr           381
#endif

static volatile int done;

struct sched_attr {
     __u32 size;

     __u32 sched_policy;
     __u64 sched_flags;

     /* SCHED_NORMAL, SCHED_BATCH */
     __s32 sched_nice;

     /* SCHED_FIFO, SCHED_RR */
     __u32 sched_priority;

     /* SCHED_DEADLINE (nsec) */
     __u64 sched_runtime;
     __u64 sched_deadline;
     __u64 sched_period;
};

int sched_setattr(pid_t pid,
               const struct sched_attr *attr,
               unsigned int flags)
{
     return syscall(__NR_sched_setattr, pid, attr, flags);
}

int sched_getattr(pid_t pid,
               struct sched_attr *attr,
               unsigned int size,
               unsigned int flags)
{
     return syscall(__NR_sched_getattr, pid, attr, size, flags);
}

void *run_deadline(void *data)
{
     struct sched_attr attr;
     int x = 0;
     int ret;
     unsigned int flags = 0;

     printf("deadline thread started [%ld]\n", gettid());

     attr.size = sizeof(attr);
     attr.sched_flags = 0;
     attr.sched_nice = 0;
     attr.sched_priority = 0;

     /* This creates a 10ms/30ms reservation */
     attr.sched_policy = SCHED_DEADLINE;
     attr.sched_runtime = 10 * 1000 * 1000;
     attr.sched_period = attr.sched_deadline = 30 * 1000 * 1000;

     ret = sched_setattr(0, &attr, flags);
     if (ret < 0) {
             done = 0;
             perror("sched_setattr");
             exit(-1);
     }

     while (!done) {
             x++;
     }

     printf("deadline thread dies [%ld]\n", gettid());
     return NULL;
}

int main (int argc, char **argv)
{
     pthread_t thread;

     printf("main thread [%ld]\n", gettid());

     pthread_create(&thread, NULL, run_deadline, NULL);

     sleep(10);

     done = 1;
     pthread_join(thread, NULL);

     printf("main dies [%ld]\n", gettid());
     return 0;
}