为什么引入组调度可以参考这篇文章的讨论。核心原因是基础的调度算法都是基于任务的,如果用户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_SCHED和CONFIG_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。这些内容在后面会单独分析。