/******以下结论和代码分析都是基于最新Linux master分支(Linux5.0)******/
在支持多用户登陆的系统,假如用户A启动了99个进程,用户B只启用了1个进程 (优先级都为NICE 0). 那么按照CFS调度规则,
用户A将获得99%的CPU时间,而用户B只能获取1%的CPU时间,这显然是不合理的.
因此,组调度被引入CFS,用户A和用户B属于不同的调度组,在优先级一样的情况下,两个用户组各占用50%的CPU时间. 而用户A中的99个进程,再根据各自权重,分享这50%的CPU时间.(代码基于最新的Linux 5.0)
1. 组调度的创建
用支持组调度,需要打开宏CONFIG_FAIR_GROUP_SCHED和CONFIG_CGROUP.
用struct 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;/*task group的se数组 */
/* runqueue "owned" by this group on each CPU */
struct cfs_rq **cfs_rq;/* task gourp 的cfs数组*/
unsigned long shares;/*task group的权重 */
/*
* load_avg can be heavily contended at clock tick time, so put
* it in its own cacheline separated from the fields above which
* will also be accessed at each tick.
*/
atomic_long_t load_avg ;/*task group的 平均负载 */
#endif
};
一个task group 在每个cpu上分配一个SE和CFS.
组调度的创建函数:
cpu_cgroup_css_alloc->sched_create_group->alloc_fair_sched_group
int alloc_fair_sched_group(struct task_group *tg, struct task_group *parent)
{
struct sched_entity *se;
struct cfs_rq *cfs_rq;
int i;
/*为每个CPU分配一个CFS_RQ指针 */
tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL);
if (!tg->cfs_rq)
goto err;
/*每个cpu分配一个SE指针 */
tg->se = kcalloc(nr_cpu_ids, sizeof(se), GFP_KERNEL);
if (!tg->se)
goto err;
/*组调度的权重默认为NICE_0_LOAD */
tg->shares = NICE_0_LOAD;
/*为每个CPU分配一个CFS和SE */
for_each_possible_cpu(i) {
cfs_rq = kzalloc_node(sizeof(struct cfs_rq),
GFP_KERNEL, cpu_to_node(i));
if (!cfs_rq)
goto err;
se = kzalloc_node(sizeof(struct sched_entity),
GFP_KERNEL, cpu_to_node(i));
if (!se)
goto err_free_rq;
/*初始化cfs和se */
init_cfs_rq(cfs_rq);
/*建立se的层级关系 */
init_tg_cfs_entry(tg, cfs_rq, se, i, parent->se[i]);
init_entity_runnable_average(se);
}
return 0;
}
重点看下init_tg_cfg_entry函数
这里的cfs和se 用gcfs和gse来表示组CFS和组se
void init_tg_cfs_entry(struct task_group *tg, struct cfs_rq *cfs_rq,
struct sched_entity *se, int cpu,
struct sched_entity *parent)
{
struct rq *rq = cpu_rq(cpu);
/*建立task group与gcfs和gse的关系 */
cfs_rq->tg = tg;
cfs_rq->rq = rq;
init_cfs_rq_runtime(cfs_rq);
tg->cfs_rq[cpu] = cfs_rq;
tg->se[cpu] = se;
if (!parent) {
se->cfs_rq = &rq->cfs;
se->depth = 0;/*顶层组调度深度为0 */
} else {
se->cfs_rq = parent->my_q;/*gse的cfs_rq指向parent的my_q */
se->depth = parent->depth + 1;
}
se->my_q = cfs_rq;/*gse的CFS rq */
/* gse的权重默认为NICE_0_LOAD */
update_load_set(&se->load, NICE_0_LOAD);
se->parent = parent;
}
1.2 组调度的逻辑图
假设系统有两个CPU, 一个用户组。
cfs_rq中的nr_running和 h_nr_running 在开启组调度时,也有不同的值
nr_running: cfs_rq红黑数中的se的数量
h_nr_running: cfs_rq红黑树的se数量,再加上子CFS中的红黑树se数量。
如上图nr_running = 4, h_nr_running = 9
2. 组进程的调度
2.1 入队列
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev)
{
struct cfs_rq *cfs_rq = &rq->cfs; //从最顶层的CFS开始遍历
struct sched_entity *se;
struct task_struct *p;
#ifdef CONFIG_FAIR_GROUP_SCHED/*组调度 */
do {
struct sched_entity *curr = cfs_rq->curr;
/*从CFS上选择虚拟时间最小的SE */
se = pick_next_entity(cfs_rq, curr);
/*如果SE时Group SE,继续在group的cfs上选择进程 */
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);/*直到选者到虚拟时间最小的进程 */
/* 获取se对应的task struct */
p = task_of(se);
if (prev != p) {
struct sched_entity *pse = &prev->se;
/*这里处理pse和se不在同一个cfs的情况 */
while (!(cfs_rq = is_same_group(se, pse))) {
int se_depth = se->depth;
int pse_depth = pse->depth;
if (se_depth <= pse_depth) {
put_prev_entity(cfs_rq_of(pse), pse);
pse = parent_entity(pse);
}
if (se_depth >= pse_depth) {
set_next_entity(cfs_rq_of(se), se);
se = parent_entity(se);
}
}
/*最后pse和se指向同一个cfs_rq */
put_prev_entity(cfs_rq, pse);
set_next_entity(cfs_rq, se);
}
/*开启高精度定时器 */
if (hrtick_enabled(rq))
hrtick_start_fair(rq, p);
return p;
}
通过pick函数后,每一级的cfs_rq->curr都指向当前红黑树中的虚拟运行时间最小的SE
比如系统有一个用户组A, se属于group A, 而pse属于系统的cfs.
那么系统的cfs_rq->curr= group se, 而gse->curr = se;
2.2 出队列
组调度的dequeue比较简单,从最底层依次遍历se.
static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
/*遍历se */
for_each_sched_entity(se) {
cfs_rq = cfs_rq_of(se);
/*把se移除cfs队列 */
dequeue_entity(cfs_rq, se, flags);
cfs_rq->h_nr_running--;
/* 如果cfs的weight不为0,证明此cfs队列上面还有其他se,此时group se不能被移除CFS队列 */
if (cfs_rq->load.weight) {
/* Avoid re-evaluating load for this entity: */
se = parent_entity(se);
break;
}
}
}
3.用户组的权重与负载计算
CFS调度器根据权重来分配CPU时间,当开启组调度后,需要根据组的权重来分配CPU时间
task group的shares来表示组的权重,初始化为NICE_0_LOAD, 也可以通过接口设置用户组的权重。
3.1 用户组进程时间片计算
se->load.weight gse->load.weight
time = sched_period * ------------------------- * ------------------------
gcfs_rq->load.weight cfs_rq->load.weight
先计算gcfs_rq的总时间片sched_period,然后计算se在组内的权重比例,再计算组在cfs_rq中的比重
函数sched_slice用于计算se时间片
static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
u64 slice = __sched_period(cfs_rq->nr_running + !se->on_rq);
/*向上依次遍历se */
for_each_sched_entity(se) {
struct load_weight *load;
struct load_weight lw;
cfs_rq = cfs_rq_of(se);
load = &cfs_rq->load;/* 获取cfs的总load*/
if (unlikely(!se->on_rq)) {
lw = cfs_rq->load;
update_load_add(&lw, se->load.weight);
load = &lw;
}
/*计算se在cfs中的比例 */
slice = __calc_delta(slice, se->load.weight, load);
}
return slice;
}
3.2 group se权重计算
计算group se的权重,可以用来跟踪task group的平均负载
gse->load.weight = task_group->shares * (gcfs->load.weight / sum_gcfs->load.weight)
由于cfs_rq->load.weight变化很快,还有与其他CPU进行互斥,会影响系统性能, 在PELT算法中,load_avg的最大值是
load->weight .所以这里以load_avg来近似代替load.weight .
gse->load.weight = task_group->shares * (gcfs->load_avg/ task_group->load_avg)
为了处理group从空闲到运行进程时出现突变,已经group load->weight为0的情况,最终gse 优化计算公式
函数update_cfs_group中会计算gse的权重.这里不再分析
3.3 task group的负载
为了不影响系统性能,task group的负载不是实时更新的,而是逐步累计到task group ->load_avg
update_load_avg->update_tg_load_avg
static inline void update_tg_load_avg(struct cfs_rq *cfs_rq, int force)
{
long delta = cfs_rq->avg.load_avg - cfs_rq->tg_load_avg_contrib;
/*只有当delta大于一定值时,cfs_rq的负载,才会累加到task group */
if (force || abs(delta) > cfs_rq->tg_load_avg_contrib / 64) {
atomic_long_add(delta, &cfs_rq->tg->load_avg);
cfs_rq->tg_load_avg_contrib = cfs_rq->avg.load_avg;
}
}