deadline调度器

Linux内核中定义了5个调度器类,分别对应5个调度器,调度优先级顺序由高到低依次为:stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。

其中dl_sched_class和rt_sched_class分别对应dl调度器和rt调度器,为实时任务所采用的调度器。相对于rt_sched_class调度类,dl_sched_class调度类中的任务调度顺序更加优先,会抢占rt_sched_class调度类和fair_sched_class调度类的任务,更早的得到cpu调度。

本文介绍dl_sched_class调度类任务及其调度算法。

Deadline调度策略介绍

实时系统除了要求在确定的时间期限内做出响应外,还要求在确定的时间期限内完成任务,所谓的确定的时间期限,我们称之为deadline。在Linux内核中,rt调度器是根据任务优先级进行调度,dl调度器,顾名思义,是根据任务的deadline进行调度。

使用dl调度器的任务,我们将其抽象为三个元素:

  • period:调度周期,即该任务需要被调度的周期时间;
  • runtime:每周期内的运行时间,即该任务在该调度周期内的运行时间,其实多数任务在每周期内运行时间并不会完全相同,所以,在dl调度器的语境下,每周期内的运行时间,通常指的是最坏执行时间,即WCET(worst-case execution time);
  • deadline:每周期的截止时间,即该任务在一个调度周期内,必须在截止时间之前完成任务,在dl调度器的开发中,deadline可以与period相同,在代码中称作“implicit deadline”,deadline也可以小于period,称作“constrained deadline”。

其中: runtime <= deadline <= period

这三个元素的具体关系可以见下图:

下面我们看一下dl调度器具体是如何工作的,我们假设一个场景,包括三个周期性的任务,其参数如下:

Task

Runtime(WCET)

Period

T1

1

4

T2

2

6

T3

3

8

    

为了简单起见,本例中的任务为之前面提到过的“implicit deadline”,即deadline等于period。

那么该三个任务在dl调度器中的工作状态可以如下图所示:

通过上图可知,三个任务都在deadline之前完成了各自的任务,周而复始,循环往复。从这点也可以看出,dl调度器更加兼顾了多个实时任务同时满足时间需求的场景,这点上是要优于rt调度器的。

这个例子中,如果三个任务都运行在同一个CPU上,那么CPU的利用率为:

U = 1/4 + 2/6 + 3/8 = 23/24

那么单核的CPU利用率的通用公式为:

 

 

从公式中可以看出,在单核系统中,当CPU利用率小于1(如果考虑到rt throttling,那么这个数值为95%),dl调度器可以满足要求,如果大于1(如果考虑到rt throttling,那么这个数值为95%)的情况下,dl调度器就不能满足如此多的任务同时运行了。

在多处理器的环境内进行dl的全局调度,事情会变得更加的复杂,一个申请dl调度器的任务的准入公式为:

即每个任务的运行时间/周期的总和应该小于或等于处理器的数目M,减去最大的利用率Umax乘以(M-1)。

从上述介绍可知,deadline调度策略本质上是一个装箱问题(bin packing),即将若干个实时任务(小箱子)装到若干个大箱子(处理器)中,该问题到目前为止,只有近似解而没有精确解。

 

dl调度器所使用的数据结构

本节介绍dl调度器中最常用的几个变量,如下图所示,dl调度器中每一个任务被认为一个调度实体,由一个结构表示,即struct sched_dl_entity,如下图所示:

struct sched_dl_entity {
     struct rb_node          rb_node;
 
      /*
       * Original scheduling parameters. Copied here from sched_attr
       * during sched_setattr(), they will remain the same until
       * the next sched_setattr().
       */
      u64             dl_runtime; /* Maximum runtime for each instance    */      --- (1)
      u64             dl_deadline;    /* Relative deadline of each instance   */    --- (2)
      u64             dl_period;  /* Separation of two instances (period) */         --- (3)
      u64             dl_bw;      /* dl_runtime / dl_period       */
      u64             dl_density; /* dl_runtime / dl_deadline     */
  
      /*
       * Actual scheduling parameters. Initialized with the values above,
       * they are continuously updated during task execution. Note that
       * the remaining runtime could be < 0 in case we are in overrun.
       */每周期的截止时间
      s64             runtime;    /* Remaining runtime for this instance  */        --- (4)
      u64             deadline;   /* Absolute deadline for this instance  */         --- (5)
     
      ...
      struct hrtimer          dl_timer;                                            ---(6)
      ...
}
  1. dl_runtime,每周期内的运行时间,即该任务在该调度周期内的运行时间,其实多数任务在每周期内运行时
  2. 的截止时间,dl_deadline的数值为从运行周期开始的相对时间。
  3. dl_period,调度周期,即该任务需要被调度的周期时间。
  4. runtime,剩余运行时间,每个任务在每个周期内的运行时间的总量为dl_runtime,runtime为dl_runtime减去该任务已经运行的时间。
  5. deadline,每周期的截止时间的绝对时间。
  6. dl_timer,每个调度实体都包含一个timer,时间为dl_period。

此外,每个CPU的dl调度队列(runqueue)使用红黑树来管理这些调度实体(sched_dl_entity),该红黑树使用deadline作为索引,最左面的调度实体的deadline最小,即应该被最先调度。

dl调度器的代码实现

dl调度器实现了两个算法来管理调度队列中的任务:

  • EDF,Early deadline first。
  • CBS,Constant bindwidth server。

下面我们逐一做代码分析:

所谓EDF,即最早的deadline的任务优先得到调度,在dl调度器算法中,主要依靠每个CPU调度队列的红黑树来实现,首先我们来看一下调度器的入队函数,即enqueue_task:

static void __enqueue_dl_entity(struct sched_dl_entity *dl_se)
{
    struct dl_rq *dl_rq = dl_rq_of_se(dl_se);
    struct rb_node **link = &dl_rq->root.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct sched_dl_entity *entry;
    int leftmost = 1;

    BUG_ON(!RB_EMPTY_NODE(&dl_se->rb_node));

    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct sched_dl_entity, rb_node);
        if (dl_time_before(dl_se->deadline, entry->deadline))
            link = &parent->rb_left;
        else {
            link = &parent->rb_right;
            leftmost = 0;
        }
    }

    rb_link_node(&dl_se->rb_node, parent, link);
    rb_insert_color_cached(&dl_se->rb_node, &dl_rq->root, leftmost);

    inc_dl_tasks(dl_se, dl_rq);
}

 

可以看到,当把一个任务的调度实体放置到红黑树中的时候,是根据该任务的deadline,把该调度实体放到红黑树的相应位置,也就是左面的节点都小于该调度实体的deadline,右面的节点都大于该调度实体的deadline。

在入队操作中,dl调度器还要调用函数replenish_dl_entity,在一个新的调度周期内重新设置调度实体的runtime和deadline,代码如下:

static void replenish_dl_entity(struct sched_dl_entity *dl_se,
                 struct sched_dl_entity *pi_se)
{
	...
    while (dl_se->runtime <= 0) {
         dl_se->deadline += pi_se->dl_period;
         dl_se->runtime += pi_se->dl_runtime;
    }
    ...
}

当进行任务切换的时候,调度器选择红黑树最左面的节点进行调度,见代码:

static struct sched_dl_entity *pick_next_dl_entity(struct rq *rq,
                            struct dl_rq *dl_rq)
 {
     struct rb_node *left = rb_first_cached(&dl_rq->root); 
 
     if (!left)
         return NULL;
 
     return rb_entry(left, struct sched_dl_entity, rb_node);     --- (1)
 }
 
 static struct task_struct *
 pick_next_task_dl(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
 {
     struct sched_dl_entity *dl_se;
     struct dl_rq *dl_rq = &rq->dl;
     struct task_struct *p;
 
     WARN_ON_ONCE(prev || rf);
 
     if (!sched_dl_runnable(rq))
         return NULL;
 
     dl_se = pick_next_dl_entity(rq, dl_rq);                      --- (2)
     BUG_ON(!dl_se); 
     p = dl_task_of(dl_se);                                     --- (3)
     set_next_task_dl(rq, p, true);
     return p;
 }
  1. 取得红黑树的最左面的节点,并获取该节点上的调度实体。
  2. 返回该调度实体。
  3. 通过调度实体获取该任务的task_struct,该任务即即将取得CPU的任务。

通过上述操作,dl调度器实现了early deadline first,即EDF。那么调度器在什么时机进行任务的调度呢,还记得sched_dl_entity中的dl_timer么,这个timer以period为周期,定期触发,在该timer触发之后,说明其所属的调度实体的新的一个周期开始了。该timer的回调函数实现如下:

static enum hrtimer_restart dl_task_timer(struct hrtimer *timer)
{
    struct sched_dl_entity *dl_se = container_of(timer,
                             struct sched_dl_entity,
                             dl_timer);
    struct task_struct *p = dl_task_of(dl_se);
    ...
    enqueue_task_dl(rq, p, ENQUEUE_REPLENISH);             --- (1)
    if (dl_task(rq->curr))
        check_preempt_curr_dl(rq, p, 0);
    else
        resched_curr(rq);                                  --- (2)

    ...
}
  1. 将该调度实体插入到调度队列的红黑树中,在入队操作中,还要重新设置该调度实体的runtime和deadline。
  2. 设置标志,在中断返回时进行上下文的切换。

dl_timer的设置函数为start_dl_timer:

static int start_dl_timer(struct task_struct *p)
 {
     struct sched_dl_entity *dl_se = &p->dl;
     struct hrtimer *timer = &dl_se->dl_timer;
     struct rq *rq = task_rq(p);
     ktime_t now, act;
     s64 delta;
     ...
     act = ns_to_ktime(dl_next_period(dl_se));    --- (1)
     now = hrtimer_cb_get_time(timer);
     delta = ktime_to_ns(now) - rq_clock(rq);
     act = ktime_add_ns(act, delta);              --- (2)
 
     ...
     if (!hrtimer_is_queued(timer)) {
         get_task_struct(p);
         hrtimer_start(timer, act, HRTIMER_MODE_ABS_HARD);   ---(3)
     }
 
     return 1;
 }
  1. 获取该任务调度实体的运行周期。
  2. 计算下一次周期开始的时间。
  3. 启动dl_timer。

该函数会在两个时间被调用,一个是该任务用完了所有的运行时间,即task_tick_dl中调用,另外一个是在该任务在runtime之内就完成了工作,主动调用sched_yield放弃CPU时调用,其中task_tick_dl和sched_yield涉及到CBS的内容,会在接下来的内容具体分析。

当实时应用在runtime耗尽之前就完成了工作,需要调用sched_yield函数来告诉调度器这个情况,调度器会将当前任务调离调度队列,同时,为该任务设置下一个运行周期的timer,代码如下:

static void yield_task_dl(struct rq *rq)
 {
     /*
      * We make the task go to sleep until its current deadline by
      * forcing its runtime to zero. This way, update_curr_dl() stops
      * it and the bandwidth timer will wake it up and will give it
      * new scheduling parameters (thanks to dl_yielded=1).
      */
     rq->curr->dl.dl_yielded = 1;                       ---(1)
 
     update_rq_clock(rq);
     update_curr_dl(rq);                                ---(2)
     /*
      * Tell update_rq_clock() that we've just updated,
      * so we don't do microscopic update in schedule()
      * and double the fastpath cost.
      */
     rq_clock_skip_update(rq);


 static void update_curr_dl(struct rq *rq)
 {
     struct task_struct *curr = rq->curr;
     struct sched_dl_entity *dl_se = &curr->dl;
     ...
 throttle:
     if (dl_runtime_exceeded(dl_se) || dl_se->dl_yielded) {
         dl_se->dl_throttled = 1;
 
         /* If requested, inform the user about runtime overruns. */
         if (timerdl_runtime_exceeded(dl_se) &&
             (dl_se->flags & SCHED_FLAG_DL_OVERRUN))
             dl_se->dl_overrun = 1;
 
         __dequeue_task_dl(rq, curr, 0);                            ---(3)
         if (unlikely(dl_se->dl_boosted || !start_dl_timer(curr)))  ---(4)
             enqueue_task_dl(rq, curr, ENQUEUE_REPLENISH);
 
         if (!is_leftmost(curr, &rq->dl))
             resched_curr(rq);
     } 
     ...
 }
  1. 设置dl_yield标志位,表示任务在耗尽runtime之前主动放弃了CPU。
  2. 调用update_curr_dl。
  3. 将当前任务调离调度队列。
  4. 为当前任务设置下一个运行周期的timer。

另一种情况,在实时任务耗尽runtime之后,仍然没有完成任务,继续使用CPU,如果不加以制止,会引起多米诺效应,导致调度队列中其他的实时任务也相继无法满足deadline的时间要求。所以,在dl调度器中,开发了CBS协议,在任务耗尽了其声明的runtime之后,强制将其调离调度队列,将CPU交给其他的任务。

调度器在上下文切换之前,会启动一个hrtick,设置该任务的最坏情况运行时间:

pick_next_task_dl
--> set_next_task_dl
   --> start_hrtick_dl

static void start_hrtick_dl(struct rq *rq, struct task_struct *p)
{
    hrtick_start(rq, p->dl.runtime);
}

可以看到,tick的设定时间为runtime。

dl调度器响应hrtick的回调函数是task_tick_dl,当tick到时之后,dl调度器同样的,将当前任务调离调度队列,同时,为该任务设置下一个运行周期的timer。

此外,还有两个问题需要我们思考一下,一个是任务调用sched_yield释放CPU,那么,整个调度器的带宽就不像声明的那样紧张了,是不是可以把这些带宽留给那些runtime不够用的任务。社区正在计划开发GRUB(Greedy Reclaim of Unused Bandwidth)协议,这个GRUB跟boot loader不是一码事儿。

另一个是CPU Frequency的变化,当CPU主频改变的时候,实时任务在同样的runtime中所计算的效率就不一样了,所以在dl调度器中,根据CPU主频的变化,任务的runtime会进行调整,具体的代码这里就不分析了,感兴趣的读者可以自行阅读代码分析。

dl调度器任务的设置和部署

实时任务的三个元素period,runtime和deadline需要在任务创建的时候使用系统调用sched_setattr声明,代码示例如下:

struct sched_attr attr;
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime =  100;
attr.sched_deadline =  800;
attr.sched_period =  1000;

sched_setattr(0, &attr, 0);

此外,还可以通过chrt命令设置实时任务的参数,如:

chrt -d --sched-runtime 5000000 --sched-deadline 10000000 --sched-period 15000000 0 ./dl_task

内核调用__sched_setscheduler将sched_attr中的参数设置到实时任务的sched_dl_entity中:

static int __sched_setscheduler(struct task_struct *p,
                 const struct sched_attr *attr,
                 bool user, bool pi)
{
...
    if ((dl_policy(policy) || dl_task(p)) && sched_dl_overflow(p, policy, attr)) {   --- (1)
        retval = -EBUSY;
        goto unlock;
}
...
    __setscheduler(rq, p, attr, pi);        --- (2)
    ...
}
static void __setscheduler(struct rq *rq, struct task_struct *p,
                const struct sched_attr *attr, bool keep_boost)
{
     __setscheduler_params(p, attr);         --- (3)
     if (dl_prio(p->prio))
         p->sched_class = &dl_sched_class;   --- (4)
     ...
}

static void __setscheduler_params(struct task_struct *p,
         const struct sched_attr *attr)
{
if (dl_policy(policy))
         __setparam_dl(p, attr);              --- (5)
}

void __setparam_dl(struct task_struct *p, const struct sched_attr *attr)
{
    struct sched_dl_entity *dl_se = &p->dl;

    dl_se->dl_runtime = attr->sched_runtime;     --- (6)
    dl_se->dl_deadline = attr->sched_deadline;
    dl_se->dl_period = attr->sched_period ?: dl_se->dl_deadline;
    dl_se->flags = attr->sched_flags;
    dl_se->dl_bw = to_ratio(dl_se->dl_period, dl_se->dl_runtime);
    dl_se->dl_density = to_ratio(dl_se->dl_deadline, dl_se->dl_runtime);
}
  1. 首先调用sched_dl_overflow检查dl调度器是否有足够带宽接受新任务,如果带宽不足则返回EBUSY。
  2. 调用__setscheduler。
  3. 继续调用__setscheduler_params。
  4. 设置任务所述的sched class为dl_sched_class。
  5. 调用 __setparam_dl。
  6. 在 __setparam_dl中,设置sched_dl_entity的内容。

至此,一个能满足实时需求,且系统带宽可以支持的任务就创建好了。

在实际工程中,有工作很繁重的任务,比如ratio=runtime/period接近为1,也有工作很轻松的任务,ratio=runtime/period远远小于1,在这种情况下,我们通常会使用cgroup将这样的大小任务分别运行在不同的CPU核上,以达到最好的带宽利用:

# cd /sys/fs/cgroup/cpuset/ 
# mkdir partition 
# cd partition/ 
# echo 1 > cpuset.cpu_exclusive 
# echo 0 > cpuset.mems 
# echo 0 > cpuset.cpus 
# echo $$ > tasks 

完成上面的准备工作之后,最后一步就是在shell中启动deadline任务。 

注意,使用dl调度器的任务不支持CPU Affinity,所以绑定CPU的话,只能使用cgroup。

dl调度器的运行日志分析

最后,我们使用社区的测试工具cyclicdeadline,结合ftrace log,来看一下dl调度器具体的运行状况,cyclicdeadline和ftrace具体的原理和操作将在后面的章节进行讲述,本节只讲述本节使用的命令和日志分析。

为简便起见,我们只在CPU3上创建一个dl实时任务,具体信息如下:

  1. 首先我们使用cyclicdeadline创建了一个实时任务,该任务的period和deadline为1000us,runtime为600us,同时使用trace-cmd来打开ftrace做日志记录。
  2. 在331651.657514,dl_timer到时,触发dl_task_timer,开始一个新的周期。
  3. dl_task_timer将该实时任务入队,然后dl调度器在中断返回是进行上下文切换,该实时任务开始运行。
  4. 该实时任务完成工作,调用sched_yield放弃CPU。
  5. 上一个中断的1000us之后,也就是331651.658514,下一个周期开始。

这种情况是实时任务调用sched_yield主动放弃CPU,如果出现任务超出runtime,调度器如何处理呢,我们将cyclicdeadline代码中的sched_yield注释掉,看看dl调度器如何工作:

 

  1. 我们仍然使用cyclicdeadline创建了一个实时任务,该任务的period和deadline为1000us,runtime为600us,但这次我们模仿一个工作繁重的任务,cyclicdeadline不会调用sched_yield来主动放弃CPU,同时使用trace-cmd来打开ftrace做日志记录。
  2. 在418779.486658,dl_timer到时,触发dl_task_timer,开始一个新的周期。
  3. dl_task_timer将该实时任务入队,然后dl调度器在中断返回是进行上下文切换,该实时任务开始运行,该任务在418779.486666开始运行。
  4. 600us以后,也就是418779.487263,hrtick到时,dl调度器调用task_tick_dl,将该任务调离调度队列。
  5. 在418779.487658,也就是上一个周期的1000us之后,新的周期开始。

 

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值