Deadline 任务调度 【ChatGPT】

发布时间 2023-12-11 20:41:38作者: 摩斯电码

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" 微秒内是可用的。为了实现这种行为,每次任务唤醒时,调度器会计算一个与保证一致的 "调度截止时间"(使用 CBS[2,3] 算法)。然后使用 EDF[1] 在这些调度截止时间上调度任务(选择具有最早调度截止时间的任务进行执行)。需要注意的是,如果使用适当的 "准入控制" 策略(参见第 4 节 "带宽管理"),则任务实际上在 "deadline" 内接收到 "runtime" 时间单位(显然,如果系统过载,无法遵守此保证)。

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

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

  • 每个 SCHED_DEADLINE 任务由 "runtime"、"deadline" 和 "period" 参数描述;

  • 任务的状态由 "调度截止时间" 和 "剩余运行时间" 描述。这两个参数最初设置为 0;

  • 当 SCHED_DEADLINE 任务唤醒(准备执行)时,调度器检查是否满足以下条件:

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

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

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

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

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

    remaining runtime = remaining runtime - t
    

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

  • 当剩余运行时间变为小于或等于 0 时,任务被称为 "被限制"(在实时文献中也称为 "耗尽"),并且直到其调度截止时间之前都无法调度。此任务的 "补充时间"(见下一项)设置为等于当前值的调度截止时间;

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

    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 |
                    ------------

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

  • 活跃的竞争中:如果它准备执行(或正在执行);

  • 活跃的非竞争中:如果它刚刚阻塞并且尚未超过 0 拖延时间;

  • 无效:如果它被阻塞并且已经超过 0 拖延时间。

状态转换:

  • 当任务阻塞时,它不会立即变为无效,因为不能立即回收其带宽,否则会破坏实时保证。因此,它进入一个称为活跃的非竞争中的过渡状态。调度器设置 "无效计时器" 在 0 拖延时间时触发,此时可以回收任务的带宽而不会破坏实时保证。

    进入活跃的非竞争状态的任务的 0 拖延时间计算如下:

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

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

  • 如果任务在无效计时器触发之前唤醒,则任务重新进入活跃的竞争状态,且 "无效计时器" 被取消。此外,如果任务在不同的运行队列上唤醒,则必须从先前运行队列的活跃利用中移除任务的利用,并添加到新运行队列的活跃利用中。为了避免任务在一个运行队列上唤醒时,"无效计时器" 在不同 CPU 上运行时出现竞争,使用 "dl_non_contending" 标志来指示任务不在运行队列上但是活跃的(因此,当任务阻塞时设置该标志,当 "无效计时器" 触发或任务唤醒时清除该标志)。

  • 当 "无效计时器" 触发时,任务进入无效状态,并且其利用从运行队列的活跃利用中移除。

  • 当无效任务唤醒时,它进入活跃的竞争状态,并且其利用被添加到其被排队的运行队列的活跃利用中。

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

  • 活跃带宽(running_bw):这是所有处于活跃状态的任务的带宽之和(即活跃的竞争中或活跃的非竞争中);

  • 总带宽(this_bw):这是所有 "属于" 运行队列的任务的带宽之和,包括无效状态的任务。

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

该算法通过减少执行任务 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
  • 时间 t = 0:

    两个任务都准备好执行,因此处于活跃竞争状态。
    假设任务 T1 是第一个开始执行的任务。
    由于没有无效任务,它的运行时间减少为 dq = -1 dt。

  • 时间 t = 2:

    假设任务 T1 阻塞。
    任务 T1 因此进入活跃非竞争状态。由于其剩余运行时间为 2,其 0 拖延时间为 t = 4。
    任务 T2 开始执行,由于没有无效任务,其运行时间仍然减少为 dq = -1 dt。

  • 时间 t = 4:

    这是任务 T1 的 0 拖延时间。由于在此期间它没有被唤醒,它进入无效状态。其带宽从 running_bw 中移除。
    任务 T2 继续执行。然而,由于 Uinact = 0.5,其运行时间现在减少为 dq = -0.5 dt。
    任务 T2 因此回收了任务 T1 未使用的带宽。

  • 时间 t = 8:

    任务 T1 唤醒。它再次进入活跃竞争状态,running_bw 增加。

2.3 节能调度

当选择 cpufreq 的 schedutil 调度器时,SCHED_DEADLINE 实现了 GRUB-PA [19] 算法,将 CPU 运行频率降低到最小值,以满足截止期限。目前,这种行为仅在 ARM 架构中实现。

在更改频率所需的时间与保留期的数量级相同时,需要特别小心。在这种情况下,设置固定的 CPU 频率会导致较少的截止期限错过。

3. 实时任务调度

警告
本节包含对经典截止期调度理论的(不全面)总结,以及它如何适用于 SCHED_DEADLINE。如果只对调度策略的使用感兴趣,读者可以“安全地”跳到第 4 节。无论如何,我们强烈建议在满足测试的冲动之后回到这里继续阅读(一旦满足了测试的冲动,?),以确保完全理解所有技术细节。

对于可以利用这种新调度纪律的任务类型没有限制,即使必须说,它特别适用于需要对其时间行为进行保证的周期性或间歇性实时任务,例如多媒体、流媒体、控制应用等。

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 是任务的相对截止期限。总之,实时任务可以描述为

Task = (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,则 EDF 能够在 CPU 上执行的所有任务的总利用率小于或等于 1 时,EDF 才能够满足所有任务的所有截止期限。如果对某些任务有 D_i != P_i,则可以将任务的密度定义为 WCET_i/min{D_i,P_i},并且只有当在 CPU 上运行的所有任务的密度之和小于或等于 1 时,EDF 才能够满足所有任务的所有截止期限:

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

重要的是要注意,这个条件只是充分条件,而不是必要条件:有些任务集是可调度的,但不满足这个条件。例如,考虑任务集 {Task_1,Task_2},其中 Task_1=(50ms,50ms,100ms) 和 Task_2=(10ms,100ms,100ms)。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 进行比较。如果对于所有可能的 t 的值,h(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”)和小于第一个任务周期的周期。因此,如果所有任务在同一时间 t 激活,全局 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)。

全局 EDF 的更复杂的可调度性测试已经在实时文献中开发[8,9],但它们不是基于总利用率(或密度)与固定常数的简单比较。如果所有任务都有 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调度参数与实时任务参数之间的关系

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

  • 运行时间(runtime)>= 最坏情况执行时间(WCET)
  • 截止时间(deadline)= D
  • 周期(period)<= P

换句话说,如果运行时间 >= 最坏情况执行时间,并且周期 <= P,那么调度截止时间和绝对截止时间(d_j)就会重合,因此适当的准入控制可以确保对该任务的作业绝对截止时间得到遵守(这就是所谓的“硬调度性质”,是[2]引理1的延伸)。需要注意的是,如果运行时间 > 截止时间,准入控制肯定会拒绝该任务,因为不可能遵守其时间约束。

参考文献:

  1. C. L. Liu 和 J. W. Layland. Scheduling algorithms for multiprogramming in a hard-real-time environment. Journal of the Association for Computing Machinery, 20(1), 1973.
  2. L. Abeni, G. Buttazzo. Integrating Multimedia Applications in Hard Real-Time Systems. Proceedings of the 19th IEEE Real-time Systems Symposium, 1998. http://retis.sssup.it/~giorgio/paps/1998/rtss98-cbs.pdf
  3. L. Abeni. Server Mechanisms for Multimedia Applications. ReTiS Lab Technical Report. http://disi.unitn.it/~abeni/tr-98-01.pdf
  4. J. Y. Leung 和 M.L. Merril. A Note on Preemptive Scheduling of Periodic, Real-Time Tasks. Information Processing Letters, vol. 11, no. 3, pp. 115-118, 1980.
  5. S. K. Baruah, A. K. Mok 和 L. E. Rosier. Preemptively Scheduling Hard-Real-Time Sporadic Tasks on One Processor. Proceedings of the 11th IEEE Real-time Systems Symposium, 1990.
  6. S. K. Baruah, L. E. Rosier 和 R. R. Howell. Algorithms and Complexity Concerning the Preemptive Scheduling of Periodic Real-Time tasks on One Processor. Real-Time Systems Journal, vol. 4, no. 2, pp 301-324, 1990.
  7. S. J. Dhall 和 C. L. Liu. On a real-time scheduling problem. Operations research, vol. 26, no. 1, pp 127-140, 1978.
  8. T. Baker. Multiprocessor EDF and Deadline Monotonic Schedulability Analysis. Proceedings of the 24th IEEE Real-Time Systems Symposium, 2003.
  9. T. Baker. An Analysis of EDF Schedulability on a Multiprocessor. IEEE Transactions on Parallel and Distributed Systems, vol. 16, no. 8, pp 760-768, 2005.
  10. J. Goossens, S. Funk 和 S. Baruah, Priority-Driven Scheduling of Periodic Task Systems on Multiprocessors. Real-Time Systems Journal, vol. 25, no. 2–3, pp. 187–205, 2003.
  11. R. Davis 和 A. Burns. A Survey of Hard Real-Time Scheduling for Multiprocessor Systems. ACM Computing Surveys, vol. 43, no. 4, 2011. http://www-users.cs.york.ac.uk/~robdavis/papers/MPSurveyv5.0.pdf
  12. U. C. Devi 和 J. H. Anderson. Tardiness Bounds under Global EDF Scheduling on a Multiprocessor. Real-Time Systems Journal, vol. 32, no. 2, pp 133-189, 2008.
  13. P. Valente 和 G. Lipari. An Upper Bound to the Lateness of Soft Real-Time Tasks Scheduled by EDF on Multiprocessors. Proceedings of the 26th IEEE Real-Time Systems Symposium, 2005.
  14. J. Erickson, U. Devi 和 S. Baruah. Improved tardiness bounds for Global EDF. Proceedings of the 22nd Euromicro Conference on Real-Time Systems, 2010.
  15. G. Lipari, S. Baruah, Greedy reclamation of unused bandwidth in constant-bandwidth servers, 12th IEEE Euromicro Conference on Real-Time Systems, 2000.
  16. L. Abeni, J. Lelli, C. Scordino, L. Palopoli, Greedy CPU reclaiming for SCHED DEADLINE. In Proceedings of the Real-Time Linux Workshop (RTLWS), Dusseldorf, Germany, 2014.
  17. L. Abeni, G. Lipari, A. Parri, Y. Sun, Multicore CPU reclaiming: parallel or sequential?. In Proceedings of the 31st Annual ACM Symposium on Applied Computing, 2016.
  18. J. Lelli, C. Scordino, L. Abeni, D. Faggioli, Deadline scheduling in the Linux kernel, Software: Practice and Experience, 46(6): 821-839, June 2016.
  19. C. Scordino, L. Abeni, J. Lelli, Energy-Aware Real-Time Scheduling in the Linux Kernel, 33rd ACM/SIGAPP Symposium On Applied Computing (SAC 2018), Pau, France, April 2018.

4. 带宽管理

如前所述,为了使-deadline调度有效和有用(即能够在“截止时间”内提供“运行时间”单位),重要的是要有一些方法来控制将CPU时间的可用分数分配给各种任务。这通常被称为“准入控制”,如果不执行准入控制,那么就无法保证-deadline任务的实际调度。

正如在第3节中已经说明的,正确调度一组实时任务必须遵守的一个必要条件是总利用率小于M。在谈论-deadline任务时,这要求所有任务的运行时间与周期之比的总和小于M。注意,运行时间/周期的比率等同于“传统”实时任务的利用率,通常也被称为“带宽”。用于控制可以分配给-deadline任务的CPU带宽的接口类似于已用于实时组调度的-rt任务的接口(又名RT-throttling - 请参阅实时组调度),并且基于procfs中的可读/可写控制文件(用于系统范围的设置)。注意,尚未为-deadline任务定义每组设置(通过cgroupfs进行控制),因为需要进行更多讨论,以便弄清楚我们希望如何在任务组级别管理SCHED_DEADLINE带宽。

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

4.1 系统范围设置

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

目前,-rt旋钮用于-deadline准入控制,并且-deadline运行时间计入-rt运行时间。我们意识到这并不完全理想;然而,现在最好有一个小的接口,并且以后能够轻松更改。理想的情况(见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 任务接口

指定在每个实例中执行给定运行时间量的周期/间歇性任务,并根据其自身时间约束的紧急性进行调度,通常需要声明:

  • 一个(最大/典型)实例执行时间,
  • 连续实例之间的最小间隔,
  • 每个实例必须完成的时间约束。

因此:

  • 提供一个包含所有必要字段的新结构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任务最多可以使用根域组成的CPU数量乘以95%。这意味着非-deadline任务将至少获得5%的CPU时间,并且-deadline任务将以保证的最坏情况延迟获得其运行时间,以满足“截止时间”参数。如果“截止时间”=“周期”,并且使用cpuset机制实现分区调度(请参见第5节),那么带宽管理的这种简单设置能够确定地保证-deadline任务将在一个周期内获得其运行时间。

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

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 # 现在实际上指定任务亲和性是多余的

6. 未来计划

仍然缺少:

  • 从程序化角度检索当前运行时间和绝对截止时间的方法
  • 对截止时间继承的改进,特别是关于在非交互任务之间保留带宽隔离的可能性。这正在从理论和实际角度进行研究,希望我们很快能够产生一些示范性代码;
  • 基于(c)组的带宽管理,以及可能的调度;
  • 针对非根用户的访问控制(以及相关的安全问题),这是允许非特权使用机制的最佳方式,以及如何防止非根用户“欺骗”系统?

正如已经讨论的,我们计划将这项工作与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调度,每100毫秒执行10毫秒。第二个线程,以SCHED_FIFO优先级10调度,每150毫秒执行20毫秒。测试将运行5秒钟。

更有趣的是,可以使用json文件描述配置,并将其作为输入传递给rt-app,类似于以下方式:

# rt-app my_config.json

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

第二个测试应用程序是schedtool的修改版本,称为schedtool-dl,可用于为特定pid/应用程序设置SCHED_DEADLINE参数。schedtool-dl可在以下位置找到:https://github.com/scheduler-tools/schedtool-dl.git

使用方法很简单:

# schedtool -E -t 10000000:100000000 -e ./my_cpuhog_app

通过上述命令,my_cpuhog_app将在每100毫秒内运行10毫秒的SCHED_DEADLINE保留中运行(请注意,参数以微秒表示)。您还可以使用schedtool为已运行的应用程序创建保留,只要您知道其pid:

# schedtool -E -t 10000000:100000000 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;
}