截止时间任务调度

0. 警告

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

1. 概述

sched_dl 调度类中包含的 SCHED_DEADLINE 策略基本上是“最早截止时间优先 (EDF)”调度算法的实现,并增加了一种机制(称为恒定带宽服务器,CBS),使得可以隔离任务之间的行为。

2. 调度算法

2.1 主要算法

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

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

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

  • 每个 SCHED_DEADLINE 任务的特征在于“运行时”、“截止时间”和“周期”参数;

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

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

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

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

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

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

  • 当 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(贪婪回收未用带宽)算法 [15, 16, 17],并且当设置了标志 SCHED_FLAG_RECLAIM 时启用。

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

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

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

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

  • ActiveNonContending:如果它刚刚阻塞并且尚未超过 0 滞后时间;

  • Inactive:如果它被阻塞并且已经超过了 0 滞后时间。

状态转换

  1. 当任务阻塞时,它不会立即变为非活动状态,因为如果不破坏实时保证,则无法立即回收其带宽。因此,它进入称为 ActiveNonContending 的过渡状态。调度器设置“非活动计时器”以在 0 滞后时间触发,此时可以回收任务的带宽而不会破坏实时保证。

    进入 ActiveNonContending 状态的任务的 0 滞后时间计算为

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

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

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

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

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

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

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

  • 总带宽 (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


- 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 进行比较。如果对于 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 参数的关系

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

  • runtime >= WCET

  • deadline = D

  • period <= P

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

参考文献

1 - C. L. Liu 和 J. W. Layland。用于硬实时环境中的多道程序设计的调度算法。《计算机协会杂志》,20(1),1973 年。

ming in a hard-real-time environment. Journal of the Association for Computing Machinery, 20(1), 1973.

2 - L. Abeni, G. Buttazzo。在硬实时系统中集成多媒体应用程序。第 19 届 IEEE 实时系统研讨会论文集,1998 年。http://retis.sssup.it/~giorgio/paps/1998/rtss98-cbs.pdf

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。多媒体应用程序的服务器机制。ReTiS 实验室技术报告。http://disi.unitn.it/~abeni/tr-98-01.pdf

Technical Report. http://disi.unitn.it/~abeni/tr-98-01.pdf

4 - J. Y. Leung 和 M.L. Merril。关于抢占式调度周期性实时任务的注释。《信息处理快报》,第 11 卷,第 3 期,第 115-118 页,1980 年。

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。在一个处理器上抢占式调度硬实时零星任务。第 11 届 IEEE 实时系统研讨会论文集,1990 年。

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。关于在一个处理器上抢占式调度周期性实时任务的算法和复杂性。《实时系统杂志》,第 4 卷,第 2 期,第 301-324 页,1990 年。

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。关于实时调度问题。《运筹学》,第 26 卷,第 1 期,第 127-140 页,1978 年。

research, vol. 26, no. 1, pp 127-140, 1978.

8 - T. Baker。多处理器 EDF 和截止时间单调可调度性分析。第 24 届 IEEE 实时系统研讨会论文集,2003 年。

Analysis. Proceedings of the 24th IEEE Real-Time Systems Symposium, 2003.

9 - T. Baker。多处理器上 EDF 可调度性分析。《IEEE 并行和分布式系统汇刊》,第 16 卷,第 8 期,第 760-768 页,2005 年。

IEEE Transactions on Parallel and Distributed Systems, vol. 16, no. 8, pp 760-768, 2005.

10 - J. Goossens、S. Funk 和 S. Baruah,多处理器上周期性任务系统的优先级驱动调度。《实时系统杂志》,第 25 卷,第 2-3 期,第 187-205 页,2003 年。

Periodic Task Systems on Multiprocessors. Real-Time Systems Journal, vol. 25, no. 2–3, pp. 187–205, 2003.

11 - R. Davis 和 A. Burns。多处理器系统的硬实时调度综述。《ACM 计算调查》,第 43 卷,第 4 期,2011 年。http://www-users.cs.york.ac.uk/~robdavis/papers/MPSurveyv5.0.pdf

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。多处理器上全局 EDF 调度下的延迟界限。《实时系统杂志》,第 32 卷,第 2 期,第 133-189 页,2008 年。

Scheduling on a Multiprocessor. Real-Time Systems Journal, vol. 32, no. 2, pp 133-189, 2008.

13 - P. Valente 和 G. Lipari。多处理器上由 EDF 调度的软实时任务的延迟上限。第 26 届 IEEE 实时系统研讨会论文集,2005 年。

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。改进的全局 EDF 延迟界限。第 22 届欧洲微型实时系统会议论文集,2010 年。

Global EDF. Proceedings of the 22nd Euromicro Conference on Real-Time Systems, 2010.

15 - G. Lipari、S. Baruah,恒定带宽服务器中未用带宽的贪婪回收,第 12 届 IEEE 欧洲微型实时系统会议,2000 年。

constant-bandwidth servers, 12th IEEE Euromicro Conference on Real-Time Systems, 2000.

16 - L. Abeni、J. Lelli、C. Scordino、L. Palopoli,SCHED DEADLINE 的贪婪 CPU 回收。在德国杜塞尔多夫举行的实时 Linux 工作坊 (RTLWS) 论文集中,2014 年。

SCHED DEADLINE. In Proceedings of the Real-Time Linux Workshop (RTLWS), Dusseldorf, Germany, 2014.

17 - L. Abeni、G. Lipari、A. Parri、Y. Sun,多核 CPU 回收:并行还是串行?。在 2016 年第 31 届 ACM 应用计算年度研讨会论文集中。

or sequential?. In Proceedings of the 31st Annual ACM Symposium on Applied Computing, 2016.

18 - J. Lelli、C. Scordino、L. Abeni、D. Faggioli,Linux 内核中的截止时间调度,《软件:实践与经验》,46(6): 821-839,2016 年 6 月。

Linux kernel, Software: Practice and Experience, 46(6): 821-839, June 2016.

19 - C. Scordino、L. Abeni、J. Lelli,Linux 内核中的节能实时调度,第 33 届 ACM/SIGAPP 应用计算研讨会 (SAC 2018),法国波城,2018 年 4 月。

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-节流 - 请参阅实时组调度),并且基于位于 procfs 中的可读/写控制文件(用于系统范围的设置)。请注意,尚未为 -deadline 任务定义每组设置(通过 cgroupfs 控制),因为需要更多讨论以弄清楚我们希望如何在任务组级别管理 SCHED_DEADLINE 带宽。

截止时间带宽管理和 RT 节流之间的主要区别在于,-deadline 任务本身具有带宽(而 -rt 任务则没有!),因此我们不需要更高级别的节流机制来强制执行所需的带宽。换句话说,这意味着接口参数仅在准入控制时使用(即,当用户调用 sched_setattr() 时)。然后,调度会考虑实际的任务参数执行,以便将 CPU 带宽分配给 SCHED_DEADLINE 任务,并在粒度方面满足其需求。因此,使用这个简单的接口,我们可以限制 -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 任务接口

指定一个定期/零星的任务,该任务在每个实例中执行给定的运行时量,并根据其自身的时间限制的紧迫性进行调度,通常需要一种声明方式

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

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

  • 每个实例必须完成的时间限制。

因此

  • 提供了一个新的 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 任务最多可以使用 95%,乘以构成每个 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)组的带宽管理,以及可能的调度;

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

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

附录 A. 测试套件

可以使用两个应用程序轻松测试 SCHED_DEADLINE 策略,这两个应用程序是更广泛的 Linux 调度器验证套件的一部分。该套件可作为 GitHub 存储库使用:https://github.com/scheduler-tools

第一个测试应用程序称为 rt-app,可用于使用特定参数启动多个线程。rt-app 支持 SCHED_{OTHER,FIFO,RR,DEADLINE} 调度策略及其相关参数(例如,niceness、优先级、运行时/截止时间/周期)。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)。

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

用法很简单

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

这样,my_cpuhog_app 将在每 100 毫秒 10 毫秒的 SCHED_DEADLINE 预留中运行(请注意,参数以纳秒为单位表示)。您还可以使用 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;
}