Linux组调度

Linux组调度通过task_group实现任务的分组管理,解决了基础调度算法中用户间CPU时间分配不均的问题。RT调度器和CFS调度器支持组调度,以RT调度为例,文章详细分析了任务组数据结构、初始化、创建、任务调度等过程,阐述了如何维护任务组优先级和调度实体的入队、出队操作。
摘要由CSDN通过智能技术生成

 为什么引入组调度可以参考这篇文章的讨论。核心原因是基础的调度算法都是基于任务的,如果用户A有10个任务在运行,用户B只有1个任务在运行,假设它们的优先级相同,那么用户A将得到10倍于用户B的CPU资源,从任务的角度看这种调度是公平的,但是从用户角度看可能并不合适。

为了解决上面的问题,Linux引入了组调度。将用户A的任务放到GroupA,用户B的任务放到GroupB,调度器保证GroupA和GroupB获得的CPU资源相同即可。

组调度是一个可选的特性,开关为CONFIG_CGROUP_SCHED,开启后,将基于CGroup机制实现一个名为"cpu"的资源控制器,该控制器实现对CPU时间的管理。目前只有RT调度类和CFS调度类支持组调度,它们的开关分别为CONFIG_RT_GROUP_SCHEDCONFIG_FAIR_GROUP_SCHED,要实现完整的组调度功能,这两个宏至少要打开一个。

这篇笔记分析了Linux组调度侧重于框架部分的实现要点,涉及到调度类部分以RT调度类为例说明,代码使用的是5.10。

数据结构

在看数据结构定义之前,我们先举一个分组示例,下面介绍各数据结构时以该示例进行说明。

一个学校的一台服务器,有Professor和Student两类用户,可以简单的按照下图创建cpu分组:

  • 不属于Professor和Student用户的任务都放到根分组中。
  • Professor和Student用户的任务各自有单独的分组。
  • 对于Professor用户,它的Mail任务和Web任务进一步划分了分组,其它任务就放到Professor分组中。
                                 <---------------->
                                 |       Root     |
                                 v----------------v
                               /                    \
                              /                      \
                      <--------------->       <---------------->
                      |   Professor   |       |     Student    |
                      v---------------v       v----------------v
                    /                   \
                   /                     \
           <---------------->      <----------------->
           |      Mail      |      |      Web        |
           v----------------v      v-----------------v        

任务组: task_group

一个task_group对象对应一个cpu控制器的CGroup分组。示例中总共会有5个task_group对象。

struct task_group {
    struct cgroup_subsys_state css; // CGroup相关成员

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* schedulable entities of this group on each cpu */
    struct sched_entity **se;
    /* runqueue "owned" by this group on each cpu */
    struct cfs_rq **cfs_rq;
#endif
#ifdef CONFIG_RT_GROUP_SCHED
    struct sched_rt_entity **rt_se;
    struct rt_rq **rt_rq;
#endif

    struct rcu_head rcu;
    struct list_head list;

    struct task_group *parent;
    struct list_head siblings;
    struct list_head children;

};

// 根分组对象
struct task_group root_task_group;

调度实体

我们知道,task_struct是以调度实体的形式加入到rq中的,支持组调度后,rq中除了有代表任务的调度实体(记作task_se)外,还可以有代表任务组的调度实体(记作group_se)。

tg->se和tg_rt_se分别代表任务组在cfs_rq和rt_rq中的调度实体。与任务在同一时刻只能在一个rq不同,任务组包含多个任务,这些任务可能位于不同的cpu上,所以一个任务组在每个cpu上都创建了一个调度实体对象。

特殊的,根分组没有代表自己的调度实体,即root_task_group.xxx_se数组均为NULL。这是因为调度实体的作用是将任务或任务组挂到运行队列中,但是根分组是不需要挂在任何队列中的。

运行队列

不支持组调度时,每个cpu上只有一个rq,该rq管理了该cpu上所有等待运行的task_se。

支持组调度后,每个任务组也用运行队列管理组内的调度实体(属于该任务组的task_se和group_se),而且也是每个cpu一个队列。tg->cfs_rq和tg->rt_rq分别管理组内的CFS调度实体和RT调度实体。

特殊的,根分组的运行队列就是cpu的运行队列,这点非常关键。

task_group树

如示例所示,系统中所有的task_group的逻辑关系是一颗倒长的树:

  • root_task_group为树根,其tg->parent为NULL,以tg->children为链表头将下一层的task_group对象组织成链表(tg->siblings)。
  • 非根分组的tg->parent指向上一层的task_group对象。

此外,系统中所有的task_group对象还会被组织到全局链表task_groups中(tg->list)。

LIST_HEAD(task_groups);

运行队列扩展

组调度特性对cfs_rq和rt_rq都做了一定扩展,增加了指向cpu运行队列和所属task_group对象的指针。

struct cfs_rq {
...
    // CFS调度实体红黑树
    struct rb_root_cached tasks_timeline;

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct rq *rq;    /* CPU runqueue to which this cfs_rq is attached */

    
    struct task_group *tg;    /* group that "owns" this runqueue */
#endif /* CONFIG_FAIR_GROUP_SCHED */
}

struct rt_rq {
    // RT调度实体优先级数组
    struct rt_prio_array active;
...


#ifdef CONFIG_RT_GROUP_SCHED
    struct rq *rq; // ---(2)
    struct task_group *tg;
#endif
};

调度实体扩展

支持组调度后,调度实体还可以代表一个任务组,所以对调度实体也进行了扩展。

struct sched_entity {
    // 将CFS调度实体加入运行队列的红黑树
    struct rb_node run_node;
...
#ifdef CONFIG_FAIR_GROUP_SCHED

    struct sched_entity *parent;
    /* rq on which this entity is (to be) queued: */
    struct cfs_rq *cfs_rq;
    /* rq "owned" by this entity/group: */
    struct cfs_rq *my_q;

#endif


};

struct sched_rt_entity {
    struct list_head run_list; // 将RT调度实体加入运行队列的优先级数组
    struct sched_rt_entity *back; // 用于调度实体的入队&出队,见下方分析
...
#ifdef CONFIG_RT_GROUP_SCHED
    struct sched_rt_entity    *parent;
    /* rq on which this entity is (to be) queued: */
    struct rt_rq *rt_rq;
    /* rq "owned" by this entity/group: */
    struct rt_rq *my_q;
#endif
};

无论是task_se还是group_se,se->parent和se->xxx_rq的含义都是一样的。se->parent指向所属任务组的group_se;xxx_rq指向所属任务组的tg->xxx_rq。

对于task_se,任务就是最小的调度实体了,所以其my_q为NULL。对于group_se,其内部还可以包含下一级的task_se和group_se,所以其my_q就是自己task_group中的tg->xxx_rq。由此可以通过my_q字段是否为NULL来判断一个调度实体到底是task_se还是group_se。

#define entity_is_task(se) (!se->my_q)

#define rq_entity_is_task(rt_se) (!(rt_se)->my_q)

开机初始化

内核启动过程中,系统只有一个根分组,所以开机时的初始化主要针对root_task_group对象进行,由sched_init()函数完成。

void __init sched_init(void)
{
    int i, j;
    unsigned long alloc_size = 0, ptr;
...
    // 为root_task_group分配运行队列和调度实体指针数组的内存
#ifdef CONFIG_FAIR_GROUP_SCHED
    alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_RT_GROUP_SCHED
    alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif

    if (alloc_size) {
        ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);

#ifdef CONFIG_FAIR_GROUP_SCHED
        root_task_group.se = (struct sched_entity **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);
        root_task_group.cfs_rq = (struct cfs_rq **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
        root_task_group.rt_se = (struct sched_rt_entity **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);
        root_task_group.rt_rq = (struct rt_rq **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_RT_GROUP_SCHED */

    }

#ifdef CONFIG_CGROUP_SCHED
    // 创建一个mem_cache优化task_group对象的内存分配
    task_group_cache = KMEM_CACHE(task_group, 0);

    // 将root_task_group对象加入全局的task_groups链表中
    list_add(&root_task_group.list, &task_groups);
    INIT_LIST_HEAD(&root_task_group.children);
    INIT_LIST_HEAD(&root_task_group.siblings);


#endif /* CONFIG_CGROUP_SCHED */

    // 将cpu的运行队列设置为root_task_group的运行队列
    for_each_possible_cpu(i) {
        struct rq *rq = cpu_rq(i);
#ifdef CONFIG_FAIR_GROUP_SCHED


        init_cfs_bandwidth(&root_task_group.cfs_bandwidth);
        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */


#ifdef CONFIG_RT_GROUP_SCHED
        init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);
#endif
    }

}

CGroup关键流程

系统开机后,用户态可以通过cgroupfs创建更多的分组来实现实现系统任务的cpu资源分配,下面来看一些关键流程在内核的实现,涉及到具体调度类的流程以RT调度类为例。

cpu控制器的CGroup接口定义如下:

struct cgroup_subsys cpu_cgrp_subsys = {
    // 分配新的cgroup分组时回调,该函数会分配task_group对象
    .css_alloc    = cpu_cgroup_css_alloc,
    // 在cgroup分组在系统中可见之前的回调
    .css_online    = cpu_cgroup_css_online,
    // cgroup分组在系统中将要删除之前的回调
    .css_released = cpu_cgroup_css_released,
    // cgroup分组内存被回收前的回调
    .css_free = cpu_cgroup_css_free,
    .css_extra_stat_show = cpu_extra_stat_show,
    // 任务创建时的回调
    .fork = cpu_cgroup_fork,
    // 检查是否可以将一个任务加入到该cgroup分组
    .can_attach    = cpu_cgroup_can_attach,
    // 将任务加入cgroup分组后的回调
    .attach = cpu_cgroup_attach,
    // cpu控制器定义的属性文件,用户态通过这些属性文件
    // 定制cpu控制器的行为
    .legacy_cftypes = cpu_legacy_files,
    .dfl_cftypes = cpu_files,
    .early_init    = true,
    .threaded = true,
};

创建新分组

当用户态通过mkdir(2)在cpu控制器下创建一个新的分组时,内核中的CGroup框架会调用cpu控制器的cpu_cgroup_css_alloc()回调函数完成task_group对象的创建和初始化。

static struct cgroup_subsys_state *
cpu_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
{
    // 上一级task_group对象
    struct task_group *parent = css_tg(parent_css);
    struct task_group *tg;

    if (!parent) {
        /* This is early initialization for the top cgroup */
        return &root_task_group.css;
    }

    tg = sched_create_group(parent); // 创建并初始化一个新的task_group对象
    if (IS_ERR(tg))
        return ERR_PTR(-ENOMEM);

    return &tg->css;
}

struct task_group *sched_create_group(struct task_group *parent)
{
    struct task_group *tg;

    // 从mem_cache中分配一个task_group对象
    tg = kzalloc(sizeof(*tg), GFP_KERNEL);


    //
 分别完成RT和CFS调度类相关的初始化
    if (!alloc_fair_sched_group(tg, parent))
        goto err;

    if (!alloc_rt_sched_group(tg, parent))
        goto err;

...
}

以下是RT调度类的alloc_rt_sched_group()实现:

int alloc_rt_sched_group(struct task_group *tg, struct task_group *parent)
{
    struct rt_rq *rt_rq;
    struct sched_rt_entity *rt_se;
    int i;

    // 为task_group对象分配运行队列指针数组和调度实体指针数组。
    tg->rt_rq = kzalloc(sizeof(rt_rq) * nr_cpu_ids, GFP_KERNEL);
    tg->rt_se = kzalloc(sizeof(rt_se) * nr_cpu_ids, GFP_KERNEL);

    for_each_possible_cpu(i) {
        // 为task_group对象分配每个cpu上的调度实体和运行队列
        rt_rq = kzalloc_node(sizeof(struct rt_rq),
             GFP_KERNEL, cpu_to_node(i));
        rt_se = kzalloc_node(sizeof(struct sched_rt_entity),
             GFP_KERNEL, cpu_to_node(i));

        init_rt_rq(rt_rq, cpu_rq(i)); // 初始化运行队列
        
        // 设置task_group中的运行实体、调度实体以及parent字段
        init_tg_rt_entry(tg, rt_rq, rt_se, i, parent->rt_se[i]);
    }
...
}

void init_tg_rt_entry(struct task_group *tg, struct rt_rq *rt_rq,
        struct sched_rt_entity *rt_se, int cpu,
        struct sched_rt_entity *parent)
{
    struct rq *rq = cpu_rq(cpu);
    
    rt_rq->highest_prio.curr = MAX_RT_PRIO;
    rt_rq->rt_nr_boosted = 0;
    rt_rq->rq = rq;
    rt_rq->tg = tg;
    
    tg->rt_rq[cpu] = rt_rq;
    tg->rt_se[cpu] = rt_se;

    if (!rt_se)
        return;

    if (!parent)
        rt_se->rt_rq = &rq->rt;
    else
        rt_se->rt_rq = parent->my_q;

    rt_se->my_q = rt_rq;
    rt_se->parent = parent;
    INIT_LIST_HEAD(&rt_se->run_list);
}

注意:该阶段的初始化只是创建了task_group对象并对其进行了初始化,还没有将其加入到系统的task_groups链表和task_group树中。

新分组关联到系统

这其实是完成用户态mkdir(2)的第二步,该流程结束后,用户态就可以看到创建的新分组了。

/* Expose task group only after completing cgroup initialization */
static int cpu_cgroup_css_online(struct cgroup_subsys_state *css)
{
    struct task_group *tg = css_tg(css);
    struct task_group *parent = css_tg(css->parent);

    // 只针对非根分组执行
    if (parent)
        sched_online_group(tg, parent);
...
}

void sched_online_group(struct task_group *tg, struct task_group *parent)
{
    unsigned long flags;

    spin_lock_irqsave(&task_group_lock, flags);
    // 将task_group对象加入全局task_groups链表中
    list_add_rcu(&tg->list, &task_groups);

    /* Root should already exist: */
    WARN_ON(!parent);

    // 将task_group对象加入上一级task_group对象的children链表中
    tg->parent = parent;
    INIT_LIST_HEAD(&tg->children);
    list_add_rcu(&tg->siblings, &parent->children);
    spin_unlock_irqrestore(&task_group_lock, flags);

    online_fair_sched_group(tg);
// CFS调度类相关流程,不再展开
}

注意:新建的task_group还没有包含任何调度实体,是个空分组,空分组的group_se是不会被加入到上一级task_group的运行队列中的。

添加任务到分组

用户态可以通过将任务的pid写入某个分组的tasks属性文件来将任务加入对应的分组中。对应到内核,CGroup框架会调用cpu控制器的cpu_cgroup_attach()回调函数来实现这一过程。

static void cpu_cgroup_attach(struct cgroup_taskset *tset)
{
    struct task_struct *task;
    struct cgroup_subsys_state *css;

    // 可以一次操作多个任务。move操作意味着会将任务从原来
    // 的分组删除,然后添加到当前分组
    cgroup_taskset_for_each(task, css, tset)
        sched_move_task(task);
}

void sched_move_task(struct task_struct *tsk)
{
    int queued, running, queue_flags =
        DEQUEUE_SAVE | DEQUEUE_MOVE | DEQUEUE_NOCLOCK;
    struct rq_flags rf;
    struct rq *rq;

    rq = task_rq_lock(tsk, &rf);
    update_rq_clock(rq);

    // 将任务从原来的运行队列中删除
    running = task_current(rq, tsk);
    queued = task_on_rq_queued(tsk);

    if (queued)
        dequeue_task(rq, tsk, queue_flags);
    if (running)
        put_prev_task(rq, tsk);

    // 修改任务的分组
    sched_change_group(tsk, TASK_MOVE_GROUP);

    // 将任务加入到新任务组的运行队列中
    if (queued)
        enqueue_task(rq, tsk, queue_flags);
    if (running) {
        // 改变分组并不会改变任务运行的cpu,所以对于正在运行的任务,
        // 要调用set_next_task()使其继续运行,只不过重新触发一次调度
        set_next_task(rq, tsk);

        resched_curr(rq);
    }

    task_rq_unlock(rq, tsk, &rf);
}

static void sched_change_group(struct task_struct *tsk, int type)
{
    struct task_group *tg;

    
    tg = container_of(task_css_check(tsk, cpu_cgrp_id, true),
            struct task_group, css);
    tg = autogroup_task_group(tsk, tg);
    tsk->sched_task_group = tg;

#ifdef CONFIG_FAIR_GROUP_SCHED
    if (tsk->sched_class->task_change_group)
        tsk->sched_class->task_change_group(tsk, type);
    else
#endif
        set_task_rq(tsk, task_cpu(tsk));
}

最终调用的是入队和出队流程,可以结合下面的分析进行理解。

任务创建

新创建的任务和其父任务属于同一个分组,大多数字段继承即可,不需要再进行特殊设置。

/*
 * This is called before wake_up_new_task(), therefore we really only
 * have to set its group bits, all the other stuff does not apply.
 */
static void cpu_cgroup_fork(struct task_struct *task)
{
    struct rq_flags rf;
    struct rq *rq;

    rq = task_rq_lock(task, &rf);

    update_rq_clock(rq);
    sched_change_group(task, TASK_SET_GROUP);

    task_rq_unlock(rq, task, &rf);
}

组调度关键流程

支持组调度后,对下面3个流程的影响最大:

  • 任务入队列。
  • 任务出队列。
  • 任务选择。

下面以RT调度类的实现为例来理解组调度的核心思想。

任务入队列

这里要特别说明一下,调度器调度执行的一定是任务,所以执行入队的一定是task_se,组调度影响的是运行队列对调度实体的管理。

static void
enqueue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_rt_entity *rt_se = &p->rt;
    enqueue_rt_entity(rt_se, flags);
...
}

static void enqueue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
    struct rq *rq = rq_of_rt_se(rt_se);

    // 按照"先出队,再入队"的方式完成调度实体的入队
    dequeue_rt_stack(rt_se, flags);
    for_each_sched_rt_entity(rt_se)
        __enqueue_rt_entity(rt_se, flags);
    enqueue_top_rt_rq(&rq->rt);
}

以最开始的示例分组为例,现在要将Mail分组中的一个任务入队列,应该要完成如下操作:

  • 将任务的task_se加入Mail分组的rt_rq中,并更新Mail分组运行队列中的任务个数统计信息。
  • 如果Mail分组的group_se尚未加入Professor分组的rt_rq中,则将其加入;然后刷新Professor分组运行队列中的任务个数统计信息。
  • 如果Professor分组的group_se尚未加入根分组的rt_rq中,将将其加入;然后刷新根分组运行队列中的任务个数统计信息。

也就是要自底向上的将每一层的调度实体都加入上一层task_group的运行队列中,并刷新各层运行队列中的任务个数统计信息。RT调度类在实现上述操作时,使用了“先出队,再入队”的方式:

  •  按照从顶层到底层的顺序将各层task_group的调度实体出队,由于是自顶向下进行的,随着出队过程,各层运行队列上维护的任务个数统计信息都会被刷新为删除该task_group的状态。
/*
 * Because the prio of an upper entry depends on the lower
 * entries, we must remove entries top - down.
 */
static void dequeue_rt_stack(struct sched_rt_entity *rt_se, unsigned int flags)
{
    struct sched_rt_entity *back = NULL;

    // 从task_se开始沿着parent指针向上遍历,最终通过back指针
    // 得到一个从顶层到底层的调度实体链表。back最后指向根分组中该分组的
    // 祖先分组的调度实体,对应到示例中就是Professor分组的调度实体
    for_each_sched_rt_entity(rt_se) {
        rt_se->back = back;
        back = rt_se;
    }

    // 将顶层队列(即rq->rt)从rq中移除。由于顶层的rt_rq是内嵌到
    // rq中的,所以这一步并非真的出队,而是将rq->rt的任务数量从
    // rq中递减,然后将rq->rt.rt_queue设置为0,表示已出队
    dequeue_top_rt_rq(rt_rq_of_se(back));

    // 从顶层到底层的顺序将各层调度实体出队列,随着出队过程,
    // 各层运行队列上的任务计数信息都将刷新为没有该分组的状态。
    // 这一步操作是将整个分组从上一层出队,分组内部任务个数及
    // 队列状态信息并不会发生变化
    for (rt_se = back; rt_se; rt_se = rt_se->back) {
        if (on_rt_rq(rt_se))
            __dequeue_rt_entity(rt_se, flags);
    }
}

static void __dequeue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
    // 调度实体所在rt_rq,即上一层调度实体的运行队列
    struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
    struct rt_prio_array *array = &rt_rq->active;

    // 将调度实体从rt_rq中删除
    if (move_entity(flags)) {
        WARN_ON_ONCE(!rt_se->on_list);
        __delist_rt_entity(rt_se, array);
    }
    rt_se->on_rq = 0;

    // 将rt_se的任务个数统计信息从rt_rq中减去
    dec_rt_tasks(rt_se, rt_rq);
}
  •   按照从底层到顶层的顺序将各层调度实体入队列,这样在每一层调度实体入队时,它运行队列上的任务个数信息已经包含了底层的调度实体。
static void __enqueue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
    // 上一层调度实体的运行队列,即调度实体将要加入的rt_rq
    struct rt_rq *rt_rq = rt_rq_of_se(rt_se);
    struct rt_prio_array *array = &rt_rq->active;
    // 调度实体自己的运行队列
    struct rt_rq *group_rq = group_rt_rq(rt_se);
    struct list_head *queue = array->queue + rt_se_prio(rt_se);

    // 空的group_se或者限流状态(RT带宽控制)的group_se不会被加入队列
    if (group_rq && (rt_rq_throttled(group_rq) || !group_rq->rt_nr_running)) {
        if (rt_se->on_list)
            __delist_rt_entity(rt_se, array);
        return;
    }

    // 将调度实体加入上一层调度实体的运行队列中
    if (move_entity(flags)) {
        WARN_ON_ONCE(rt_se->on_list);
        if (flags & ENQUEUE_HEAD)
            list_add(&rt_se->run_list, queue);
        else
            list_add_tail(&rt_se->run_list, queue);

        __set_bit(rt_se_prio(rt_se), array->bitmap);
        rt_se->on_list = 1;
    }
    rt_se->on_rq = 1;

    // 将rt_se的任务个数统计信息累加到rt_rq上
    inc_rt_tasks(rt_se, rt_rq);
}

任务出队列

调度框架会调用RT调度类的dequeue_task_rt()回调函数完成RT任务的出队。

static void dequeue_task_rt(struct rq *rq, struct task_struct *p, int flags)
{
    struct sched_rt_entity *rt_se = &p->rt;
...
    dequeue_rt_entity(rt_se, flags);
}

static void dequeue_rt_entity(struct sched_rt_entity *rt_se, unsigned int flags)
{
    struct rq *rq = rq_of_rt_se(rt_se);

    // "先出队,再入队"
    dequeue_rt_stack(rt_se, flags);
    for_each_sched_rt_entity(rt_se) {
        // 调度实体自己的运行队列
        struct rt_rq *rt_rq = group_rt_rq(rt_se);
        // 对于task_se,rt_rt=NULL;
        // 对于group_se,rt_rq->rt_nr_running为0说明是空分组
        // 上面两种情况不再入队,相当于完成了出队
        if (rt_rq && rt_rq->rt_nr_running)
            __enqueue_rt_entity(rt_se, flags);
    }
    enqueue_top_rt_rq(&rq->rt);
}

出队和入队面临着同一个问题,就是要刷新各层调度实体的运行队列上的任务个数统计信息,所以也采取了“先出队,再入队”的方式。不同的是在再次入队时,任务和空分组不再入队,相当于完成了出队。

任务选择

支持组调度后,RT调度类在选择任务时必须要找到一个task_se。在搜索task_group树时,在遇到group_se时,必须继续查找该group_se,直到找到最高优先级的task_se为止。

static struct task_struct *pick_next_task_rt(struct rq *rq)
{
    struct task_struct *p;

    // rq上没有任务要运行
    if (!sched_rt_runnable(rq))
        return NULL;

    // 选择优先级最高的任务
    p = _pick_next_task_rt(rq);
    // 准备运行任务
    set_next_task_rt(rq, p, true);
    return p;
}

static struct task_struct *_pick_next_task_rt(struct rq *rq)
{
    struct sched_rt_entity *rt_se;
    struct rt_rq *rt_rq  = &rq->rt;

    // 从最顶层的rq->rt开始,找到最高优先级的调度实体,
    // 如果该调度实体是group_se,继续从该group_se的rt_rq中
    // 查找最高优先级的调度实体,以此类推,知道找到task_se为止,
    // 这样找到的task_se一定是rq上优先级最高的任务
    do {
        rt_se = pick_next_rt_entity(rq, rt_rq);
        BUG_ON(!rt_se);
        rt_rq = group_rt_rq(rt_se);
    } while (rt_rq);

    return rt_task_of(rt_se);
}

// 返回rt_rq上优先级最高的调度实体
static struct sched_rt_entity *pick_next_rt_entity(struct rq *rq,
                           struct rt_rq *rt_rq)
{
    struct rt_prio_array *array = &rt_rq->active;
    struct sched_rt_entity *next = NULL;
    struct list_head *queue;
    int idx;

    idx = sched_find_first_bit(array->bitmap);
    BUG_ON(idx >= MAX_RT_PRIO);

    queue = array->queue + idx;
    next = list_entry(queue->next, struct sched_rt_entity, run_list);

    return next;
}

总结

总结一下,组调度有如下要点:

  • 组调度将系统中的任务划分到一个个的任务组中,系统中的所有任务组逻辑上构成一颗倒长的树,每个任务都属于唯一的一个任务组。
  • 原来每个cpu只有一个运行队列,支持组调度后,运行队列扩展到了任务组,每个任务组都有NR_CPUS个运行队列,分别用来管理任务组内在各个cpu上运行的任务。
  • 组调度将调度实体扩展到任务组,运行队列上除了是任务调度实体外,还可以是任务组调度实体。

这篇笔记重点分析了支持组调度后,调度器是如何管理任务的,以及组调度对一些调度流程的影响,限于篇幅,并未涉及组调度的实际使用。简单来说,CFS调度类利用组调度在各个任务组之间合理的分配cpu资源;RT调度类利用组调度进行带宽控制,防止RT任务长时间占用cpu。这些内容在后面会单独分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值