一次cfs组调度不公平引起的负载不均衡分析及cfs组调度深入探索(三)

文章目录

    • 第一篇中复现程序触发问题逻辑以及 patch-1 的修复逻辑
      • 1 创建任务组
      • 2 设置一个任务到任务组中
      • 3 设置睡眠任务 cpuset,触发负载不能正确衰减
      • 4 patch 修复逻辑

第一篇中复现程序触发问题逻辑以及 patch-1 的修复逻辑

1 创建任务组

首先按照脚本逻辑创建任务组,接着将睡眠任务设置到对应任务组,最后设置睡眠任务 cpuset时有以下流程逻辑:

首先是:创建任务组,当前创建的是cfs 任务组时将会有如下调用:
alloc_fair_sched_group -------------------------------(1)
	-> init_tg_cfs_entry -----------------------------(2)
    -> init_entity_runnable_average ------------------(3)

(1)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;

	tg->cfs_rq = kcalloc(nr_cpu_ids, sizeof(cfs_rq), GFP_KERNEL); /* 1 */
	if (!tg->cfs_rq)
		goto err;
	tg->se = kcalloc(nr_cpu_ids, sizeof(se), GFP_KERNEL);		/* 2 */
	if (!tg->se)
		goto err;

	tg->shares = NICE_0_LOAD;									/* 3 */

	init_cfs_bandwidth(tg_cfs_bandwidth(tg));

	for_each_possible_cpu(i) {									/* 4 */
		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;

		init_cfs_rq(cfs_rq);
		init_tg_cfs_entry(tg, cfs_rq, se, i, parent->se[i]);
		init_entity_runnable_average(se);
	}

	return 1;

err_free_rq:
	kfree(cfs_rq);
err:
	return 0;
}

1)2)为一个新的任务的指针数组分配空间,后续每一个 cpu 的 cfs_rq/se 地址都存在这个里面。
3)一个新的任务组对应的 shares 权重默认为 1024。
4)对每一个cpu分配 cfs_rq 和 se,并初始化它们。
(2)init_tg_cfs_entry

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);

	cfs_rq->tg = tg;	// 设置 cfs_rq 对应的任务组
	cfs_rq->rq = rq;	// 对应的 rq
	init_cfs_rq_runtime(cfs_rq);

	tg->cfs_rq[cpu] = cfs_rq;	// 设置任务组的 cfs_rq[cpu]
	tg->se[cpu] = se;			// 设置任务组的 se[cpu]

	/* se could be NULL for root_task_group */
	if (!se)
		return;

	if (!parent) {
		se->cfs_rq = &rq->cfs;	// 对于根 root_task_group 普通任务的 cfs_rq 就是就绪队列的
		se->depth = 0;			// cfs,并且深度为零。
	} else {
		se->cfs_rq = parent->my_q;	// 这里可以看到任务组的se->cfs_rq指向自己所属的任务组cfs_rq[cpu]
		se->depth = parent->depth + 1;	// 任务组 se[cpu] 的深度
	}

	se->my_q = cfs_rq;	// 参与挂入红黑树对应的 cfs_rq。
	/* guarantee group entities always have weight */
	update_load_set(&se->load, NICE_0_LOAD); // 设置 se 权重默认为 NICE_0_LOAD,对应任务组将会在后续 update_cfs_group 时根据任务组 shares 和 load_avg 更新权重。
	se->parent = parent;
}

(3)init_entity_runnable_average 为 se 设置新的任务负载,具体逻辑可以之前对 init_entity_runnable_average 函数的描述及分析。
至此当创建一个新的任务组时对应的 cfs_rq/se 的初始化完成。接着开始将任务附加到任务组中。

2 设置一个任务到任务组中

在脚本中有如下示例:
任务组所属层次:

cgroup
    -> zy_test
    	-> sub-1
    		-> test_sub

我们将任务通过写 cgoup.procs 文件设置到 test_sub 任务组中:触发如下逻辑:

cgroup_attach_task
	-> cgroup_migrate_prepare_dst
	-> cgroup_migrate
		-> cgroup_migrate_execute
			-> cpu_cgroup_attach
				-> sched_move_task	-------------------------1-> task_change_group_fair
						-> task_move_group_fair -------------2-> cgroup_migrate_finish

(1)当设置 cgroup.procs 时将会调用 sched_move_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);  ------------------1)
	queued = task_on_rq_queued(tsk);  ------------------2if (queued)
		dequeue_task(rq, tsk, queue_flags);
	if (running)
		put_prev_task(rq, tsk);

	sched_change_group(tsk, TASK_MOVE_GROUP); ----------3----------------------------------------------------4if (queued)
		enqueue_task(rq, tsk, queue_flags);
	if (running)
		set_curr_task(rq, tsk);

	task_rq_unlock(rq, tsk, &rf);
}

(1)(2)首先根据 rq->curr和p->on_rq 判断任务是否正在运行和在就绪队列上。如果在队列上我们需要先将它出队,如果正在运行我们需要更新任务的运行信息。
(4)完成组切换后如果任务在队列上,我们需要将它重新入对,在运行则设置一下当前任务。设置任务对应不同调度类意义不同,fair 任务可能会进行任务带宽相关检测。出对入队的rq相同为什么需要做这个操作呢?可能是为了触发一些对应调度类或者实体的信息更新。这里fair中则会有一个影响服务器负载更新操作。
(3)调用 task_move_group_fair 更新任务的 cfs_rq 和 se。

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

	/*
	 * All callers are synchronized by task_rq_lock(); we do not use RCU
	 * which is pointless here. Thus, we pass "true" to task_css_check()
	 * to prevent lockdep warnings.
	 */
	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); // 调用对应调度类的 change 函数,这里是 task_move_group_fair。
	else
#endif
		set_task_rq(tsk, task_cpu(tsk));
}
static void task_move_group_fair(struct task_struct *p)
{
	detach_task_cfs_rq(p); ---------------------------1set_task_rq(p, task_cpu(p)); ---------------------2#ifdef CONFIG_SMP
	/* Tell se's cfs_rq has been changed -- migrated */
	p->se.avg.last_update_time = 0; ------------------3#endif
	attach_task_cfs_rq(p); ---------------------------4}

(1)detach_task_cfs_rq 更新原有 cfs_rq 信息。

static void detach_task_cfs_rq(struct task_struct *p)
{
	struct sched_entity *se = &p->se;
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

	if (!vruntime_normalized(p)) {
		/*
		 * Fix up our vruntime so that the current sleep doesn't
		 * cause 'unlimited' sleep bonus.
		 */
		place_entity(cfs_rq, se, 0);
		se->vruntime -= cfs_rq->min_vruntime;	// 判断是否是一个普通的出队操作,如果不是则会根据唤醒等操作信息对运行时间进行补偿等。
	}

	detach_entity_cfs_rq(se);	// 更新 cfs_rq 负载等信息。
}
static void detach_entity_cfs_rq(struct sched_entity *se)
{
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

	/* Catch up with the cfs_rq and remove our load when we leave */
	update_load_avg(cfs_rq, se, 0);	// 更新 cfs_rq 任务组等负载。
	detach_entity_load_avg(cfs_rq, se);	// 将负载从原有 cfs_rq 中移除se的负载。
	update_tg_load_avg(cfs_rq, false);	// 更新对应任务组 load_avg,false表示不满足阈值不更新。
	propagate_entity_cfs_rq(se);	// 这里顺便从 se->parent 开始调用 update_load_avg 对每个 cfs_rq 任务组进行更新。
}
static void propagate_entity_cfs_rq(struct sched_entity *se)
{
	struct cfs_rq *cfs_rq;

	/* Start to propagate at parent */
	se = se->parent;

	for_each_sched_entity(se) {		// 沿着 se->parent 向上更新每一个 cfs_rq 直到 root
		cfs_rq = cfs_rq_of(se);

		if (cfs_rq_throttled(cfs_rq))	// 节流了那就不需要往上了,一个节流上面肯定也不能运行。
			break;

		update_load_avg(cfs_rq, se, UPDATE_TG);	//更新 cfs_rq,并且还要更新任务组load_avg。
	}
}

(2)设置任务新的 se和cfs_rq,假设任务组后续通过 cpuset 设置为只能在 cpu10 运行,而此时睡眠任务处于 cpu30:

static inline void set_task_rq(struct task_struct *p, unsigned int cpu)
{
#if defined(CONFIG_FAIR_GROUP_SCHED) || defined(CONFIG_RT_GROUP_SCHED)
	struct task_group *tg = task_group(p);
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
	set_task_rq_fair(&p->se, p->se.cfs_rq, tg->cfs_rq[cpu]); /* 1 */
	p->se.cfs_rq = tg->cfs_rq[cpu]; 						/* 2 */
	p->se.parent = tg->se[cpu];
#endif

#ifdef CONFIG_RT_GROUP_SCHED
	p->rt.rt_rq  = tg->rt_rq[cpu];
	p->rt.parent = tg->rt_se[cpu];
#endif
}

1)se->avg.last_update_time 将会更新到新的cfs_rq->avg.last_update_time,保持同步。
2)此时任务还处于cpu30,所以任务的 p->se.cfs_rq = tg->cfs_rq[30]。后续设置 cpuset 时将会把p->se.cfs_rq 指向 tg->cfs_rq[10]
(3)se->avg.last_update_time 设置为 0告诉cfs_rq发生改变。
(4)attach_task_cfs_rq 将 se的负载更新到新的 cfs_rq 上。

static void attach_task_cfs_rq(struct task_struct *p)
{
	struct sched_entity *se = &p->se;
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

	attach_entity_cfs_rq(se);	/* 1 */

	if (!vruntime_normalized(p))
		se->vruntime += cfs_rq->min_vruntime;	// 不能让睡眠等任务vruntime太小,避免过度抢占cpu。
}

1)操作基本和detach_task_cfs_rq 相同,只是这里会附加新负载上去。

static void attach_entity_cfs_rq(struct sched_entity *se)
{
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

#ifdef CONFIG_FAIR_GROUP_SCHED
	/*
	 * Since the real-depth could have been changed (only FAIR
	 * class maintain depth value), reset depth properly.
	 */
	se->depth = se->parent ? se->parent->depth + 1 : 0;
#endif

	/* Synchronize entity with its cfs_rq */
	update_load_avg(cfs_rq, se, sched_feat(ATTACH_AGE_LOAD) ? 0 : SKIP_AGE_LOAD);
	attach_entity_load_avg(cfs_rq, se, 0);
	update_tg_load_avg(cfs_rq, false);
	propagate_entity_cfs_rq(se);
}
static void attach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	u32 divider = LOAD_AVG_MAX - 1024 + cfs_rq->avg.period_contrib; // 由 pelt 算法得出,负载最大值为 LOAD_AVG_MAX 47742

	/*
	 * When we attach the @se to the @cfs_rq, we must align the decay
	 * window because without that, really weird and wonderful things can
	 * happen.
	 *
	 * XXX illustrate
	 */
	se->avg.last_update_time = cfs_rq->avg.last_update_time;
	se->avg.period_contrib = cfs_rq->avg.period_contrib; // 上次不到 1024 us 的周期

	/*
	 * Hell(o) Nasty stuff.. we need to recompute _sum based on the new
	 * period_contrib. This isn't strictly correct, but since we're
	 * entirely outside of the PELT hierarchy, nobody cares if we truncate
	 * _sum a little.
	 */
	se->avg.util_sum = se->avg.util_avg * divider;

	se->avg.load_sum = divider;
	if (se_weight(se)) {
		se->avg.load_sum =
			div_u64(se->avg.load_avg * se->avg.load_sum, se_weight(se));
	}

	se->avg.runnable_load_sum = se->avg.load_sum;

	enqueue_load_avg(cfs_rq, se);

	cfs_rq->avg.util_avg += se->avg.util_avg;
	cfs_rq->avg.util_sum += se->avg.util_sum;

	add_tg_cfs_propagate(cfs_rq, se->avg.load_sum);

	cfs_rq_util_change(cfs_rq, flags);
}

至此,睡眠任务负载被附加到了 cfs_rq[30] 队列上,并且如果任务触发了出队入队等操作该部分负载也会持续衰减并更新到任务组的load_avg上。

3 设置睡眠任务 cpuset,触发负载不能正确衰减

如上例子,此时睡眠任务处于 cpu30 ,现在设置任务到 cpu10运行。首先看看 cpuset 逻辑:
同 cgroup一样:

cgroup_attach_task
	-> cgroup_migrate_prepare_dst
	-> cgroup_migrate
		-> cgroup_migrate_execute
			-> cpuset_can_attach
				-> cpuset_attach
					-> set_cpus_allowed_ptr
    					-> __set_cpus_allowed_ptr
	-> cgroup_migrate_finish

可以看到最终调用了 __set_cpus_allowed_ptr 去设置任务的新cpu。部分代码如下:

static int __set_cpus_allowed_ptr(struct task_struct *p,
				  const struct cpumask *new_mask, bool check)
{
	do_set_cpus_allowed(p, new_mask);	---------------------------1)

	dest_cpu = cpumask_any_and(cpu_valid_mask, new_mask);
	if (task_running(rq, p) || p->state == TASK_WAKING) { ----------2struct migration_arg arg = { p, dest_cpu };
		/* Need help from migration thread: drop lock and wait. */
		task_rq_unlock(rq, p, &rf);
		stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);
		tlb_migrate_finish(p->mm);
		return 0;
	} else if (task_on_rq_queued(p)) { ------------------------------3/*
		 * OK, since we're going to drop the lock immediately
		 * afterwards anyway.
		 */
		rq = move_queued_task(rq, &rf, p, dest_cpu);
	}
}

(1)do_set_cpus_allowed 对任务首先进行一些更新操作。
(2)(3)如果任务正处于运行或者正在唤醒或者在队列上,那么操作很简单直接进行任务迁移

migration_cpu_stop
    -> __migrate_task (if queued)
    	-> move_queued_task
    -> p->wake_cpu = arg->dest_cp (else)

在任务正在运行的 cpu 上唤醒停机类迁移线程,把 migration_cpu_stop 挂载在迁移线程的工作链表上并在迁移线程运行时执行 migration_cpu_stop 函数完成任务迁移到目标 cpu 上的事件,示例:睡眠任务正在cpu30运行,目标cpu10,
则此时在 cpu30 上启动迁移线程,迁移线程从工作链表中取出需要调用的 migration_cpu_stop 函数执行并完成将任务迁移到 cpu10 上。
对于处于就绪队列的任务直接调用 move_queued_task 开始出队入队更新运行cpu。
对于睡眠任务会在醒来或者负载均衡和根据 cpumask重新设置新的cpu运行。

static struct rq *move_queued_task(struct rq *rq, struct rq_flags *rf,
				   struct task_struct *p, int new_cpu)
{
	lockdep_assert_held(&rq->lock);

	p->on_rq = TASK_ON_RQ_MIGRATING;	// 标记任务正在 rq 上迁移。
	dequeue_task(rq, p, DEQUEUE_NOCLOCK);	// 从原来队列出队任务。
	set_task_cpu(p, new_cpu);			/* 1 */ //从这里开始 p->cpu 以及 cfs_rq 信息被正确更新。
	rq_unlock(rq, rf);

	rq = cpu_rq(new_cpu);

	rq_lock(rq, rf);
	BUG_ON(task_cpu(p) != new_cpu);
	enqueue_task(rq, p, 0);	// 重新入队任务到新的cpu 上。
	p->on_rq = TASK_ON_RQ_QUEUED;	// 标记任务在队列上。
	check_preempt_curr(rq, p, 0);	// 在新cpu上检测抢占。

	return rq;
}

1)set_task_cpu

void set_task_cpu(struct task_struct *p, unsigned int new_cpu)
{
	if (task_cpu(p) != new_cpu) {
		if (p->sched_class->migrate_task_rq)
			p->sched_class->migrate_task_rq(p, new_cpu);	-------1)
		p->se.nr_migrations++;
		rseq_migrate(p);
		perf_event_task_migrate(p);
	}

	__set_task_cpu(p, new_cpu);	-----------------------------------2}

(1)对应睡眠任务是 migrate_task_rq_fair

static void migrate_task_rq_fair(struct task_struct *p, int new_cpu)
{
	if (p->on_rq == TASK_ON_RQ_MIGRATING) { -------------1/*
		 * In case of TASK_ON_RQ_MIGRATING we in fact hold the 'old'
		 * rq->lock and can modify state directly.
		 */
		lockdep_assert_held(&task_rq(p)->lock);
		detach_entity_cfs_rq(&p->se);

	} else {
		/*
		 * We are supposed to update the task to "current" time, then
		 * its up to date and ready to go to new CPU/cfs_rq. But we
		 * have difficulty in getting what current time is, so simply
		 * throw away the out-of-date time. This will result in the
		 * wakee task is less decayed, but giving the wakee more load
		 * sounds not bad.
		 */
		remove_entity_load_avg(&p->se); ----------------------2}

	/* Tell new CPU we are migrated */
	p->se.avg.last_update_time = 0;

	/* We have migrated, no longer consider this task hot */
	p->se.exec_start = 0;

	update_scan_period(p, new_cpu);
}

1)对于正在运行或者就绪队列上的任务直接把任务负载从对应 cfs_rq 上移除,那么原来在 cfs_rq[30]负载将被清除,并广播到对应任务组的load_avg,负载正确移除并且衰减。
2)remove_entity_load_avg 此时任务处于睡眠状态,负载不直接从cfs_rq上移除,而是挂入cfs_rq->revmoed中在后续的各种更新点中去衰减或者移除。
(2)__set_task_cpu

static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
{
	set_task_rq(p, cpu); // 此时 p->se.cfs_rq将从 cfs_rq[30]切换到我们cpuset的 cfs_rq[10]。
#ifdef CONFIG_SMP
	/*
	 * After ->cpu is set up to a new value, task_rq_lock(p, ...) can be
	 * successfuly executed on another CPU. We must ensure that updates of
	 * per-task data have been completed by this moment.
	 */
	smp_wmb();
#ifdef CONFIG_THREAD_INFO_IN_TASK
	p->cpu = cpu;	// 更新任务 cpu
#else
	task_thread_info(p)->cpu = cpu;
#endif
	p->wake_cpu = cpu;	// wake_cpu = cpu,这样后续try_to_wake_up中目标cpu一直,不会触发迁移操作。
#endif
}

对于睡眠任务,不会有这些操作,一切真正迁移在 try_to_wake_up中。
首先查看一下当前状态,由于睡眠 set_task_cpu 不会调用,不过根据cpumask可以得到 wake_cpu =10,cpu = 10,
p->se.cfs_rq[30]。当任务在cpuset 之后醒来时触发 try_to_wake_up

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
	p->sched_contributes_to_load = !!task_contributes_to_load(p);
	p->state = TASK_WAKING;

	cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags); // 得到新的运行 cpu
	if (task_cpu(p) != cpu) {
		wake_flags |= WF_MIGRATED;
		psi_ttwu_dequeue(p);
		set_task_cpu(p, cpu); // 调用 set_task_cpu 触发上面描述逻辑,负载被添加到 removed中。
	}

	ttwu_queue(p, cpu, wake_flags);
stat:
	ttwu_stat(p, cpu, wake_flags);
out:
	raw_spin_unlock_irqrestore(&p->pi_lock, flags);

	return success;
}

从上述逻辑中可以看到,对于非睡眠任务负载会直接移除,并更新整个任务组负载,如果任务睡眠,负载并不会立即移除,而是在唤醒时附加到原有 cfs_rq->removed 中。在后续合适的时机调用 update_load_avg 去移除。
至此只可以看到,如果通过cpuset方式将任务组绑定到只能在某一个cpu上,如这里,任务组只能在 cpu10运行,而实际情况是 任务组对应的cfs_rq[30]上有负载是需要衰减或者移除的。如果睡眠任务被设置到 cpu10,那么对应的 cfs_rq[30]将没有任务,并且由于cpuset的关系,cfs_rq[30]不会发生update_load_avg 逻辑,那么这部分cfs_rq贡献到 任务组 的load_avg 将无法移除并且看起来就是永久的多了一份负载,从而影响接下来任务组 se的权重计算,使得任务组下调度实体分的的时间片出现偏差。
从第三部分中也可以看到,如果 任务组只能在一个cpu上运行,那么此时cfs_rq贡献的load_avg应该等于tg->load_avg。而实际情况是 load_avg < tg->load_avg,而任务组多的这部分就是 cfs_rq[30]贡献的部分, 952 这数值从打印可以看到正是此时cfs_rq[30]的负载, 952 也是由 se负载附加到 cfs_rq[30]的。

4 patch 修复逻辑

patch 逻辑如下:

--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -10878,16 +10878,22 @@ static void propagate_entity_cfs_rq(struct sched_entity *se)
 {
 	struct cfs_rq *cfs_rq;
 
+	list_add_leaf_cfs_rq(cfs_rq_of(se)); --------------------------1+
 	/* Start to propagate at parent */
 	se = se->parent;
 
 	for_each_sched_entity(se) {
 		cfs_rq = cfs_rq_of(se);
 
-		if (cfs_rq_throttled(cfs_rq))
-			break;
+		if (!cfs_rq_throttled(cfs_rq)){
+			update_load_avg(cfs_rq, se, UPDATE_TG);
+			list_add_leaf_cfs_rq(cfs_rq);	----------------------2+			continue;
+		}
 
-		update_load_avg(cfs_rq, se, UPDATE_TG);
+		if (list_add_leaf_cfs_rq(cfs_rq))	---------------------3+			break;
 	}
 }

(1)(2)在 attach_entity_cfs_rq 中向上广播更新负载的同时把每一个 cfs_rq 按照自底向上的顺序添加到 rq->leaf_cfs_rq_list 链表中,并更新当前cfs_rq负载及对应任务组load_avg。
(3)如果不能添加到队列尾部或者插入到其中,说明自己所属的上级cfs_rq被节流不在链表中此时暂时把自己加入到孤立链表上并返回fasle,表示不能往上添加了已经节流,那么此时则直接退出循环。

static inline bool list_add_leaf_cfs_rq(struct cfs_rq *cfs_rq)
{
	struct rq *rq = rq_of(cfs_rq);
	int cpu = cpu_of(rq);

	if (cfs_rq->on_list) // 已经在链表上,与 tmp_alone_branch 比较,相等为true,表明孤立父级已被节流,不需要再往上遍历。
		return rq->tmp_alone_branch == &rq->leaf_cfs_rq_list;

	cfs_rq->on_list = 1;

	/*
	 * Ensure we either appear before our parent (if already
	 * enqueued) or force our parent to appear after us when it is
	 * enqueued. The fact that we always enqueue bottom-up
	 * reduces this to two cases and a special case for the root
	 * cfs_rq. Furthermore, it also means that we will always reset
	 * tmp_alone_branch either when the branch is connected
	 * to a tree or when we reach the top of the tree
	 */
	if (cfs_rq->tg->parent &&	// 如果父级存在并且在链表中直接把自己加入到父级的尾部。
	    cfs_rq->tg->parent->cfs_rq[cpu]->on_list) {
		/*
		 * If parent is already on the list, we add the child
		 * just before. Thanks to circular linked property of
		 * the list, this means to put the child at the tail
		 * of the list that starts by parent.
		 */
		list_add_tail_rcu(&cfs_rq->leaf_cfs_rq_list,
			&(cfs_rq->tg->parent->cfs_rq[cpu]->leaf_cfs_rq_list));
		/*
		 * The branch is now connected to its tree so we can
		 * reset tmp_alone_branch to the beginning of the
		 * list.
		 */
		rq->tmp_alone_branch = &rq->leaf_cfs_rq_list;
		return true;
	}

	if (!cfs_rq->tg->parent) { // 如果不存在父级,说明自己属于顶层,直接把自己加入rq->leaf_cfs_rq_list 作为链表遍历时的最后一个输出。
		/*
		 * cfs rq without parent should be put
		 * at the tail of the list.
		 */
		list_add_tail_rcu(&cfs_rq->leaf_cfs_rq_list,
			&rq->leaf_cfs_rq_list);
		/*
		 * We have reach the top of a tree so we can reset
		 * tmp_alone_branch to the beginning of the list.
		 */
		rq->tmp_alone_branch = &rq->leaf_cfs_rq_list;
		return true;
	}

	/*
	 * The parent has not already been added so we want to
	 * make sure that it will be put after us.
	 * tmp_alone_branch points to the begin of the branch
	 * where we will add parent.
	 */
	list_add_rcu(&cfs_rq->leaf_cfs_rq_list, rq->tmp_alone_branch); // 不能插入任何位置,暂时把自己挂入孤立链表中,一旦任何孤立链表可以插入整个队列,则在那个时候链入。
	/*
	 * update tmp_alone_branch to points to the new begin
	 * of the branch
	 */
	rq->tmp_alone_branch = &cfs_rq->leaf_cfs_rq_list;
	return false; // 返回 false,告诉调用者本次链表操作没有插入队列说明父级节流了。
}

list_add_leaf_cfs_rq 调用点如下:

enqueue_entity
tg_unthrottle_up
unthrottle_cfs_rq
enqueue_task_fair

可以看到,在任务入队,解除cfs_rq节流时,对应的cfs_rq将会加入链表。
相对应的 list_del_leaf_cfs_rq 把 cfs_rq 从链表中移除,调用点如下:

throttle_cfs_rq
update_blocked_averages
unregister_fair_sched_group

可以看到在任务节流,负载均衡更新负载期间会去移除。其中 update_blocked_averages函数也是整个链表使用地方,代码如下:

static void update_blocked_averages(int cpu)
{
	struct rq *rq = cpu_rq(cpu);
	struct cfs_rq *cfs_rq, *pos;
	const struct sched_class *curr_class;
	struct rq_flags rf;
	bool done = true;

	rq_lock_irqsave(rq, &rf);
	update_rq_clock(rq);

	/*
	 * Iterates the task_group tree in a bottom up fashion, see
	 * list_add_leaf_cfs_rq() for details.
	 */
	for_each_leaf_cfs_rq_safe(rq, cfs_rq, pos) { ----------------1struct sched_entity *se;

		if (test_cfs == cfs_rq)
			printk("zhangyi: for_each_leaf_cfs_rq_safe load_avg %ld\n", cfs_rq->avg.load_avg);

		if (update_cfs_rq_load_avg(cfs_rq_clock_task(cfs_rq), cfs_rq)) ----2update_tg_load_avg(cfs_rq, 0);

		/* Propagate pending load changes to the parent, if any: */
		se = cfs_rq->tg->se[cpu];
		if (se && !skip_blocked_update(se))
			update_load_avg(cfs_rq_of(se), se, 0); ------------------------3/*
		 * There can be a lot of idle CPU cgroups.  Don't let fully
		 * decayed cfs_rqs linger on the list.
		 */
		if (cfs_rq_is_decayed(cfs_rq)) -----------------------------------4list_del_leaf_cfs_rq(cfs_rq);

		/* Don't need periodic decay once load/util_avg are null */
		if (cfs_rq_has_blocked(cfs_rq))
			done = false;
	}
}

(1)所有加入链表的cfs_rq 将在此时自底向上依次遍历。
(2)更新对应 cfs_rq 负载,如果负载发生变化,那么接着更新 cfs_rq 对应任务组的 load_avg。update_cfs_rq_load_avg函数上面描述过,首先还会把removed上需要移除的负载移除接着再去进行负载更新。
(3)这里还会同步会更新 cfs 所属任务组的se负载,根据是否有负载广播。
(4)cfs_rq_is_decayed代码如下:

static inline bool cfs_rq_is_decayed(struct cfs_rq *cfs_rq)
{
        if (cfs_rq->load.weight)
                return false;

        if (cfs_rq->avg.load_sum)
                return false;

        if (cfs_rq->avg.util_sum)
                return false;

        if (cfs_rq->avg.runnable_load_sum)
                return false;

        return true;
}

当对应 cfs_rq 没有权重,负载,利用率说明没有任何任务,也没有任何未衰减完的负载,那么返回false,有其中任意一项则会返回true。
对于没有负载的cfs_rq那么没有必要继续留在链表上,这里将会将其移出链表。

对于 patch,在任务设置到任务组的时候在调用 attach_entity_cfs_rq 时,patch将任务对应的 cfs_rq(这里是 cfs_rq[30])将其加入到链表中,那么只要这个cfs_rq还有负载,就会在负载均衡时一直进行负载更新,直至移除负载或者衰减为0,那么此时向任务组贡献的load_avg将会正确衰减回归正确水平。此时再去sched_debug查看就可以看到,cfs_rq->avg.load_avg = tg->load_avg

你可能感兴趣的:(linux,linux,cfs,group)