Kernel源码笔记之调度:2.负载均衡

Kernel源码笔记目录

负载均衡

代码基于4.19。

负载均衡的时机

  • 在scheduler_tick时进行
  • 在schedule时,cpu队列为空时进行
  • wake_up_new_task(在fork的时候), sched_exec(执行新程序时), try_to_wake_up(唤醒一个进程时)

scheduler_tick

在scheduler_tick中,每次都会调用trigger_load_balance来进行负载均衡。

// kernel/sched/core.c

void scheduler_tick(void)
{
	...
    // 更新当前进程vruntime等统计信息,这个过程中,当前进程可能会被调度
	curr->sched_class->task_tick(rq, curr, 0);

    // 计算负载相关
	cpu_load_update_active(rq);
	calc_global_load_tick(rq);

#ifdef CONFIG_SMP
	rq->idle_balance = idle_cpu(cpu);

    // 负载均衡
	trigger_load_balance(rq);
#endif
}
// kernel/sched/fair.c
void trigger_load_balance(struct rq *rq)
{
	// todo:没看懂
	if (unlikely(on_null_domain(rq)))
		return;

    // 如果时间超过了运行队列的next_balance值,则触发软中断进行负载均衡,
	// 软中断在系统调用/中断返回前会进行检测并处理,
	// 所以这里在本次时钟中断执行完后就会执行负载均衡
	if (time_after_eq(jiffies, rq->next_balance))
		raise_softirq(SCHED_SOFTIRQ);

    // todo:没看懂
	nohz_balancer_kick(rq);
}

软中断的注册是在fair的初始化函数中,init_sched_fair_class在内核start的时候就会被调用。

// kernel/sched/fair.c
__init void init_sched_fair_class(void)
{
#ifdef CONFIG_SMP
	open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);

#ifdef CONFIG_NO_HZ_COMMON
	nohz.next_balance = jiffies;
	nohz.next_blocked = jiffies;
	zalloc_cpumask_var(&nohz.idle_cpus_mask, GFP_NOWAIT);
#endif
#endif /* SMP */

}
// kernel/sched/fair.c
static __latent_entropy void run_rebalance_domains(struct softirq_action *h)
{
	struct rq *this_rq = this_rq();

    // idle_balance的值是在scheduler_tick中设置的,如果
    // 当前cpu空闲,idle_balance为1,否则为0
	enum cpu_idle_type idle = this_rq->idle_balance ?
						CPU_IDLE : CPU_NOT_IDLE;

    // todo:没看懂
	if (nohz_idle_balance(this_rq, idle))
		return;

	// 这个函数里会更新阻塞进程的平均负载
	// todo: 没看懂
	update_blocked_averages(this_rq->cpu);

    // 真正的进行负载均衡
	rebalance_domains(this_rq, idle);
}
// kernel/sched/fair.c
static void rebalance_domains(struct rq *rq, enum cpu_idle_type idle)
{
	int continue_balancing = 1; // 继续进行平衡
	int cpu = rq->cpu; // 当前cpu
	unsigned long interval; 
	struct sched_domain *sd;
	/* Earliest time when we have to do rebalance again */
	unsigned long next_balance = jiffies + 60*HZ; // 初始化下一次要平衡的时间,60秒之后
	int update_next_balance = 0;

    //  需要同步          需要衰减
	int need_serialize, need_decay = 0;
	u64 max_cost = 0; // 进行平衡花费的时间

	rcu_read_lock();

    // 开始遍历每个调度域,这个循环自底向上遍历,
    // 因为逻辑cpu处于最下层,所以向上遍历时都会让sd=sd->parent
	for_each_domain(cpu, sd) {

        // 如果当前时间超过了next_decay_max_lb_cost,则衰减max_newidle_lb_cost,
        // max_newidle_lb_cost在idle平衡中会使用
		if (time_after(jiffies, sd->next_decay_max_lb_cost)) {
			// todo:253, 256是什么意思,没看懂
			sd->max_newidle_lb_cost =
				(sd->max_newidle_lb_cost * 253) / 256;
			sd->next_decay_max_lb_cost = jiffies + HZ;
			need_decay = 1;
		}
		max_cost += sd->max_newidle_lb_cost;

        // 如果当前调度域不允许负载均衡,则继续循环
		if (!(sd->flags & SD_LOAD_BALANCE))
			continue;

		// 如果不再需要平衡且不再衰减,则跳出循环
		if (!continue_balancing) {

            // 这里当need_decay为1时,则继续循环,相当于只执行上面对
            // max_newidle_lb_cost做衰减
			if (need_decay)
				continue;
			break;
		}

        // 获取本调度域的平衡间隔时间,其实获取的是sd->balance_interval的值,balance_interval初始化和权重值相同
		// 第二个参数标识当前cpu是否忙,如果是忙的话则是sd->busy_factor * sd->balance_interval的值
		// busy_factor的值为32,在初始化的时候定的
		// 这个间隔值最小为1, 最大为max_load_balance_interval(单位是jiffies)
		// sd->busy_factor, sd->balance_interval这些值都是毫秒为单位,这个函数会把它转化成jiffies为单位
		interval = get_sd_balance_interval(sd, idle != CPU_IDLE);

        // 如果此调度域需要同步,则要获取锁
		need_serialize = sd->flags & SD_SERIALIZE;
		if (need_serialize) {
			if (!spin_trylock(&balancing))
				goto out;
		}

        // 判断当前时间是否达到了此调度域的平衡时间点,平衡时间点为上次平衡时间+平衡间隔
		// 不能太频繁的进行平衡,否则会影响性能
		if (time_after_eq(jiffies, sd->last_balance + interval)) {

            // 真正的进行负载均衡,这个函数的返回值是pull来的进程数
			// continue_balancing这个值会在这个函数里被改
			if (load_balance(cpu, rq, sd, idle, &continue_balancing)) {
                // 如果有拉过来的任务,再判断一下当前cpu的状态
				idle = idle_cpu(cpu) ? CPU_IDLE : CPU_NOT_IDLE;
			}
            
            // 更新上次平衡的时间。last_balance设置的地方只有两个:一个是初始化,一个就是这里
			sd->last_balance = jiffies;

            // 更新平衡时间间隔,因为balance_interval在load_balance中有可能会被修改
			interval = get_sd_balance_interval(sd, idle != CPU_IDLE);
		}
		if (need_serialize)
			spin_unlock(&balancing);
out:
        // 下次均衡的时间最大不能超过调度域最后一次平衡时间+间隔
		// 这个next_balance最后会赋值给rq->next_balance,也就是当前队列下一次要进行平衡的时间
		// 从所有调度域里选择一个最小的时间点,做为此队列下一次要进行负载平衡的时间
		if (time_after(next_balance, sd->last_balance + interval)) {
			next_balance = sd->last_balance + interval;
			update_next_balance = 1;
		}
	}

	// 计算平衡的花费时间
	if (need_decay) {
		rq->max_idle_balance_cost =
			max((u64)sysctl_sched_migration_cost, max_cost);
	}
	rcu_read_unlock();

	// 更新下次平衡时间
	if (likely(update_next_balance)) {
		rq->next_balance = next_balance;

#ifdef CONFIG_NO_HZ_COMMON
		// 更新nohz下次更新时间
		if ((idle == CPU_IDLE) && time_after(nohz.next_balance, rq->next_balance))
			nohz.next_balance = rq->next_balance;
#endif
	}
}

// todo:各个平衡时间之间的关系

// kernel/sched/fair.c
/**
* this_cpu, this_rq: 要迁移的cpu号和cpu的队列
* sd: 调度域,表示在要当前调度域里做均衡
* idle: 要迁移的cpu是否空闲
* continue_balancing: 是否要继续做迁移,用做主调函数做判断
*/
static int load_balance(int this_cpu, struct rq *this_rq,
			struct sched_domain *sd, enum cpu_idle_type idle,
			int *continue_balancing)
{
	int ld_moved, cur_ld_moved, active_balance = 0;
	struct sched_domain *sd_parent = sd->parent;
	struct sched_group *group;
	struct rq *busiest;
	struct rq_flags rf;
	struct cpumask *cpus = this_cpu_cpumask_var_ptr(load_balance_mask);

	// 负载均衡时的上下文
	struct lb_env env = {
		.sd		= sd, // 当前cpu所属的调度域
		.dst_cpu	= this_cpu, // 当前cpu序号
		.dst_rq		= this_rq, // 当前cpu队列
		.dst_grpmask    = sched_group_span(sd->groups), // 当前cpu所属组内的cpu集合
		.idle		= idle, // idle状态
		.loop_break	= sched_nr_migrate_break, // 迁移中断的次数(迁移多少次后中断)
		.cpus		= cpus, // 系统可以进行负载均衡的cpu(这个参数在下面会被修改)
		.fbq_type	= all, // 负载均衡的类型(todo: 不是很明白)
		.tasks		= LIST_HEAD_INIT(env.tasks), // 初始化链表头,迁移来的任务会挂在这个链上
	};

	// 这个函数可以看成:cpus = sched_domain_span(sd) & cpu_active_mask;
	// 这里将调度域里的cpu与激活状态的cpu相与,也就是只有激活状态的cpu参与迁移。
	// cpu_active_mask记录着系统里激活状态的cpu。(todo: 还没看这个变量的修改过程)
	cpumask_and(cpus, sched_domain_span(sd), cpu_active_mask);

	// 增加负载均衡计数
	schedstat_inc(sd->lb_count[idle]);

redo:

	// 当前cpu是否应该做负载均衡,如果不合适直接退出
	if (!should_we_balance(&env)) {
		*continue_balancing = 0;
		goto out_balanced;
	}

	// 在本调度域内找一个最繁忙的调度组
	group = find_busiest_group(&env);
	if (!group) {
		// 如果没有最繁忙的组,增加一下相关变量的值,然后返回
		schedstat_inc(sd->lb_nobusyg[idle]);
		goto out_balanced;
	}

	// 在繁忙的组里找一个最忙的运行队列、
	busiest = find_busiest_queue(&env, group);
	if (!busiest) {
		// 如果没有最繁忙的队列,增加一下相关统计变量的值,然后返回
		schedstat_inc(sd->lb_nobusyq[idle]);
		goto out_balanced;
	}

	// 最繁忙的队列怎么会是dst_rq呢?很显然是个bug
	BUG_ON(busiest == env.dst_rq);

	// 把需要移动的负载量加到lb_imbalance[idle]数组里
	// todo: 不知道这个数组里记录这些数据是干什么用的
	schedstat_add(sd->lb_imbalance[idle], env.imbalance);

	// 将最繁忙的cpu和最繁忙的运行队列记录到env环境信息中
	env.src_cpu = busiest->cpu;
	env.src_rq = busiest;

	// 转移到本地cpu的任务数量
	ld_moved = 0;
	if (busiest->nr_running > 1) {
		// todo: 这个标志不啥意思?
		env.flags |= LBF_ALL_PINNED;

		// 循环的最大次数是任务数量和sysctl_sched_nr_migrate的最小值,
		// 这里的迁移次数会在迁移任务的方法里使用,肯定不能超过nr_running次,
		// 否则,只运行了那么多任务,多迁移几次没有意义
		env.loop_max  = min(sysctl_sched_nr_migrate, busiest->nr_running);

more_balance:
		// 要从繁忙队列里转移任务,肯定要先锁住繁忙对列,并且保存中断
		rq_lock_irqsave(busiest, &rf);
		update_rq_clock(busiest);

		// 从繁忙队列里出队几个任务
		cur_ld_moved = detach_tasks(&env);

		// 已经转移完了,就释放繁忙队列锁
		rq_unlock(busiest, &rf);

		// 如果有转移出来的任务,就把任务加到当前队列里
		if (cur_ld_moved) {
			// 移过来的任务加到目标队列中
			attach_tasks(&env);
			ld_moved += cur_ld_moved;
		}

		// 还原中断
		local_irq_restore(rf.flags);

		// 这个标志在detach_tasks中设置,有这个标志需要再次平衡
		// todo: 暂时不知道这个标志是啥意思
		if (env.flags & LBF_NEED_BREAK) {
			env.flags &= ~LBF_NEED_BREAK;
			goto more_balance;
		}

		// 这个标志在detach_tasks中设置,由于迁移的任务不允许在当前cpu中运行,
		// 把重新设置dst_cpu,然后再次转移任务,loop_break设置的是默认值sched_nr_migrate_break
		// 重新选择cpu,重新选择cpu后,就要重新进行选择系统组和繁忙队列
		// todo: 没太看懂
		if ((env.flags & LBF_DST_PINNED) && env.imbalance > 0) {

			/* Prevent to re-select dst_cpu via env's CPUs */
			cpumask_clear_cpu(env.dst_cpu, env.cpus);

			env.dst_rq	 = cpu_rq(env.new_dst_cpu);
			env.dst_cpu	 = env.new_dst_cpu;
			env.flags	&= ~LBF_DST_PINNED;
			env.loop	 = 0;
			env.loop_break	 = sched_nr_migrate_break;

			goto more_balance;
		}

		// 根据条件更新父调度域的不平衡标志
		if (sd_parent) {
			int *group_imbalance = &sd_parent->groups->sgc->imbalance;

			// todo: LBF_SOME_PINNED标志都没有看懂
			// 这里只更新组不平衡,
			// 猜测:经过转移任务之后,如果还没有达到平衡,则设置这个组不平衡
			if ((env.flags & LBF_SOME_PINNED) && env.imbalance > 0)
				*group_imbalance = 1;
		}

		// todo:没看懂
		if (unlikely(env.flags & LBF_ALL_PINNED)) {
			cpumask_clear_cpu(cpu_of(busiest), cpus);
			
			// todo: 没看懂
			if (!cpumask_subset(cpus, env.dst_grpmask)) {
				env.loop = 0;
				env.loop_break = sched_nr_migrate_break;
				goto redo;
			}
			goto out_all_pinned;
		}
	}

	// 下面是处理pull模式下负载均衡失败的情况,
	// 失败时将启用push模式,push模式由最繁忙的cpu主动给本cpu推任务
	if (!ld_moved) {
		// 增加负载均衡失败的计数,没有成功移出任务就被当做是失败
		schedstat_inc(sd->lb_failed[idle]);
		
		// 如果不是将要变成空闲的cpu,则增加调度域的失败次数
		// todo: 为什么只在这种情况下增加失败次数
		if (idle != CPU_NEWLY_IDLE)
			sd->nr_balance_failed++;

		if (need_active_balance(&env)) {
			unsigned long flags;

			raw_spin_lock_irqsave(&busiest->lock, flags);

			// 看目标cpu上正在运行的任务是否允许在当前cpu上运行,如果不允许则直接退出
			if (!cpumask_test_cpu(this_cpu, &busiest->curr->cpus_allowed)) {
				raw_spin_unlock_irqrestore(&busiest->lock, flags);
				// todo:这个标志啥意思?
				env.flags |= LBF_ALL_PINNED;
				goto out_one_pinned;
			}

			// 如果最忙cpu的push模式没有被激活,则激活最忙cpu的push模式,
			// 要push的目标cpu是当前cpu
			if (!busiest->active_balance) {
				busiest->active_balance = 1;
				busiest->push_cpu = this_cpu;
				active_balance = 1;
			}
			raw_spin_unlock_irqrestore(&busiest->lock, flags);

			if (active_balance) {
				// 调用stopper线程来执行push模式的负载均衡,
				// 真正执行的函数是active_load_balance_cpu_stop,
				// stopper是异步执行,但是这个函数会等到push模式的负载均衡
				// 结束之后才会返回
				stop_one_cpu_nowait(cpu_of(busiest),
					active_load_balance_cpu_stop, busiest,
					&busiest->active_balance_work);
			}

			/* We've kicked active balancing, force task migration. */
			sd->nr_balance_failed = sd->cache_nice_tries+1;
		}
	} else
		// 重置均衡失败
		sd->nr_balance_failed = 0;

	if (likely(!active_balance)) {
		// 如果没有激活push模式,则重置均衡间隔为最小间隔
		sd->balance_interval = sd->min_interval;
	} else {
		 // 如果激活了push模式,则把均衡间隔值翻倍
		 // todo: 为啥要翻倍,不明白
		if (sd->balance_interval < sd->max_interval)
			sd->balance_interval *= 2;
	}

	goto out;

// 下面三个out_标签都是处理负载均衡失败的情况
out_balanced:
	// todo: 没看懂
	if (sd_parent && !(env.flags & LBF_ALL_PINNED)) {
		// 这里是清空父调度域调度组的不平衡标志。

		// 下面这三句代码为啥不直接写成: sd_parent->groups->sgc->imbalance = 0,
		// 难道是为了可读性??
		int *group_imbalance = &sd_parent->groups->sgc->imbalance;

		if (*group_imbalance)
			*group_imbalance = 0;
	}

out_all_pinned:
	/*
	 * We reach balance because all tasks are pinned at this level so
	 * we can't migrate them. Let the imbalance flag set so parent level
	 * can try to migrate them.
	 */
	schedstat_inc(sd->lb_balanced[idle]);

	// 清空失败标志
	// 根据原文注释,这样可以让父调度域继续执行均衡操作
	sd->nr_balance_failed = 0;

out_one_pinned:
	ld_moved = 0;

	if (env.idle == CPU_NEWLY_IDLE)
		goto out;

	/* tune up the balancing interval */
	// todo: 没看懂,为什么在负载固定时要把迁移间隔翻倍
	if (((env.flags & LBF_ALL_PINNED) &&
			sd->balance_interval < MAX_PINNED_INTERVAL) ||
			(sd->balance_interval < sd->max_interval))
		sd->balance_interval *= 2;
out:
	return ld_moved;
}

static int detach_tasks(struct lb_env *env)
{
	struct list_head *tasks = &env->src_rq->cfs_tasks;
	struct task_struct *p;
	unsigned long load;
	int detached = 0;

	lockdep_assert_held(&env->src_rq->lock);

	// 如果需要移动的负载量为0,则不需要移动task,直接返回0
	if (env->imbalance <= 0)
		return 0;

	while (!list_empty(tasks)) {
		// 如果目标队列中只有一个任务或者没有,且当前队列是空闲的,则不移动了,否则会造成新的不平衡,
		// 当cpu繁忙或者可运行数量大于1时则进行迁移
		if (env->idle != CPU_NOT_IDLE && env->src_rq->nr_running <= 1)
			break;

		// 取出最后一个任务
		p = list_last_entry(tasks, struct task_struct, se.group_node);

		// 迁移次数增加1
		env->loop++;
		
		// 如果已经循环到限制值,则停止循环,直接退出
		if (env->loop > env->loop_max)
			break;

		// 如果已经循环到中断值,则退出休息一会,
		// loop_break这个变量应该是控制迁移太快吧,到达这个路径时,则退出迁移,
		// 然后由主控函数来判断是否还需要再次迁移。
		// todo: 这个路径没太看懂
		if (env->loop > env->loop_break) {
			env->loop_break += sched_nr_migrate_break;
			env->flags |= LBF_NEED_BREAK;
			break;
		}

		// 如果不能迁移则继续循环
		if (!can_migrate_task(p, env))
			goto next;

		// 计算进程的负载,如果负载为0,则归为1
		load = max_t(unsigned long, task_h_load(p), 1);

		// 负载小于16并且当前调度域没有失败过时不迁移,可能作者觉得迁移它所带来的花费会抵消优化。
		// sched_feat是调度特性,sched_feat(LB_MIN)应该是限制最小负载的迁移。(猜的)
		// todo: 没太看懂
		if (sched_feat(LB_MIN) && load < 16 && !env->sd->nr_balance_failed)
			goto next;

		// 负载的一半都比本次要迁移的不平衡负载多了,那肯定不能迁移这个进程,
		// 否则会导致新的不平衡。(猜的)
		// todo: 没太看懂,没有证据证明上面说的。
		if ((load / 2) > env->imbalance)
			goto next;

		// 走到这里就表示这进程可以迁移
		// detach_task把这个任务从当前链表脱链,再设置改变目标cpu
		detach_task(p, env->src_rq, env->dst_cpu);

		// 将这个任务加到env的task中
		list_add(&p->se.group_node, &env->tasks);

		// 增加迁移出进程的计数
		detached++;

		// 从拉取的总负载中减掉这个进程的负载
		env->imbalance -= load;

#ifdef CONFIG_PREEMPT
		// 在可抢占的内核中,如果idle类型是NEW_IDLE, 则在转移了一个任务之后,立刻返回,
		// 否则会造成延迟。
		if (env->idle == CPU_NEWLY_IDLE)
			break;
#endif

		// 需要迁移的负载够了,则退出循环
		if (env->imbalance <= 0)
			break;

		continue;
next:
		// 这里把这个任务重新加到任务列表里
		// list_move是先删除再添加
		// todo: 这里为什么要用list_move?
		list_move(&p->se.group_node, tasks);
	}

	// 增加已拉取的进程数,这些都是统计数据,
	// 打开SCHED_DEBUG可以看到这些统计数据
	schedstat_add(env->sd->lb_gained[env->idle], detached);

	// 返回已分离的进程数
	return detached;
}

static int can_migrate_task(struct task_struct *p, struct lb_env *env)
{
	int tsk_cache_hot;

	// 锁住队列
	lockdep_assert_held(&env->src_rq->lock);

	 /**
		下面几种任务不能迁移:
		
		1. cpu_allowed不允许在这个cpu上运行,
		2. 正在运行的进程
		3. 当前cpu上的cache还是热的
	 */

	// 节流进程不许迁移
	if (throttled_lb_pair(task_group(p), env->src_cpu, env->dst_cpu))
		return 0;

	
	if (!cpumask_test_cpu(env->dst_cpu, &p->cpus_allowed)) {
		// 这个分支是当前进程的cpu_allowed,不允许在目标cpu上运行,也不允许转移
		int cpu;

		// 递增因为亲和性迁移失败的次数
		schedstat_inc(p->se.statistics.nr_failed_migrations_affine);

		// todo: 这个标志没看懂
		env->flags |= LBF_SOME_PINNED;

		/*
		 * Remember if this task can be migrated to any other CPU in
		 * our sched_group. We may want to revisit it if we couldn't
		 * meet load balance goals by pulling other tasks on src_cpu.
		 *
		 * Avoid computing new_dst_cpu for NEWLY_IDLE or if we have
		 * already computed one in current iteration.
		 */
		if (env->idle == CPU_NEWLY_IDLE || (env->flags & LBF_DST_PINNED))
			return 0;

		// 重新选择dst_cpu
		for_each_cpu_and(cpu, env->dst_grpmask, env->cpus) {
			if (cpumask_test_cpu(cpu, &p->cpus_allowed)) {
				env->flags |= LBF_DST_PINNED;
				env->new_dst_cpu = cpu;
				break;
			}
		}

		
		return 0;
	}

	// 走到这里表示有任务可以迁移到目标cpu上
	env->flags &= ~LBF_ALL_PINNED;

	// 当前进程正在运行,增加因为运行而迁移失败的次数
	if (task_running(env->src_rq, p)) {
		schedstat_inc(p->se.statistics.nr_failed_migrations_running);
		return 0;
	}

	
	/*
	原文注释:强制转移的条件
	1. 目标numa节点是优选的
	2. 任务的cache是冷的
	3. 平衡失败次数太多
	*/

	// migrate_degrades_locality 只有在CONFIG_NUMA_BALANCING配置打开时才有效,否则返回-1
	/**
	migrate_degrades_locality:
	返回1: 会降低局部性,说明当前进程的缓存是热的
	返回0: 会提高局部性,说明缓存是冷的
	返回-1: 对局部性无影响
	*/
	tsk_cache_hot = migrate_degrades_locality(p, env);

	// 如果迁移此进程对局部性无影响的话,再计算一下缓存是不是热的
	if (tsk_cache_hot == -1)
		tsk_cache_hot = task_hot(p, env);

	// 下面这个if代码写的不太好。。
	// 1.如果缓存不热,则可以迁移
	// 2.如果平衡的失败次数已经超过cache_nice_tries,即使缓存是热的也进行强制迁移
	if (tsk_cache_hot <= 0 ||
	    env->sd->nr_balance_failed > env->sd->cache_nice_tries) {
		if (tsk_cache_hot == 1) {
			// 增加热缓存计数器
			schedstat_inc(env->sd->lb_hot_gained[env->idle]);

			// 增加强制迁移计算器
			schedstat_inc(p->se.statistics.nr_forced_migrations);
		}
		return 1;
	}

	// 增加因为热缓存迁移失败的计数器
	schedstat_inc(p->se.statistics.nr_failed_migrations_hot);
	return 0;
}

static int migrate_degrades_locality(struct task_struct *p, struct lb_env *env)
{
	struct numa_group *numa_group = rcu_dereference(p->numa_group);
	unsigned long src_weight, dst_weight;
	int src_nid, dst_nid, dist;

	// 如果没有使用numa平衡,则对局部性无影响
	if (!static_branch_likely(&sched_numa_balancing))
		return -1;

	// 如果 !p->numa_faults, 或者当前调度域不支持numa,则对局部性无影响
	// todo: 何为numa_faults?
	if (!p->numa_faults || !(env->sd->flags & SD_NUMA))
		return -1;

	// 取出src和dst的numa节点id
	src_nid = cpu_to_node(env->src_cpu);
	dst_nid = cpu_to_node(env->dst_cpu);

	// 如果两个cpu在同一个numa节点里,则对局部性无影响
	if (src_nid == dst_nid)
		return -1;

	// 如果源numa_id是当前进程的优先节点
	if (src_nid == p->numa_preferred_nid) {
		if (env->src_rq->nr_running > env->src_rq->nr_preferred_running)
			// 如果源队列的运行任务比最优运行数量高的话,那么迁移降低局部性
			return 1;
		else
			// 否则,对局部性无影响
			return -1;
	}

	// 如果目标numa节点是当前进程的优先节点,那对局部性有提高
	if (dst_nid == p->numa_preferred_nid)
		return 0;

	// 如果要迁移到的cpu已经空闲,则可以迁移
	// 因为如果目标cpu空闲,即使降低源cpu的局部性,也是有好处的
	if (env->idle == CPU_IDLE)
		return -1;

	// 计算两个节点之间的距离
	// todo: 计算过程没看懂,大意是如果是同一个节点,那距离为10, 否则距离为20
	dist = node_distance(src_nid, dst_nid);
	if (numa_group) {
		src_weight = group_weight(p, src_nid, dist);
		dst_weight = group_weight(p, dst_nid, dist);
	} else {
		src_weight = task_weight(p, src_nid, dist);
		dst_weight = task_weight(p, dst_nid, dist);
	}

	// 如果目标权重小于源权重会减少局部性,否则提高局部性
	return dst_weight < src_weight;
}

static int task_hot(struct task_struct *p, struct lb_env *env)
{
	s64 delta;

	// 给源队列加锁
	lockdep_assert_held(&env->src_rq->lock);

	// 如果调度类不是cfs,则返回0
	// 估计其它调度器不用负载均衡(瞎猜的)
	if (p->sched_class != &fair_sched_class)
		return 0;

	// 如果当前task有idle策略,也表示缓存是冷的
	// todo: 不明白什么意思
	if (unlikely(task_has_idle_policy(p)))
		return 0;
	
	// todo: 没看懂
	if (sched_feat(CACHE_HOT_BUDDY) && env->dst_rq->nr_running &&
			(&p->se == cfs_rq_of(&p->se)->next ||
			 &p->se == cfs_rq_of(&p->se)->last))
		return 1;

	// 迁移花费如果是-1的话,就表示缓存是热的?
	// todo: -1是啥意思?
	if (sysctl_sched_migration_cost == -1)
		return 1;
	
	// 迁移花费如果是0,则表示迁移此任务会提高局部性
	if (sysctl_sched_migration_cost == 0)
		return 0;

	// delta算的是进程开始执行到现在的时间间隔,如果这个间隔小于迁移花费值,
	// 则表示缓存是热的。
	// rq_clock_task取的是当前队列的时间
	delta = rq_clock_task(env->src_rq) - p->se.exec_start;

	return delta < (s64)sysctl_sched_migration_cost;
}

// kernel/sched/fair.c
static int should_we_balance(struct lb_env *env)
{
	struct sched_group *sg = env->sd->groups;
	int cpu, balance_cpu = -1;

	// 如果被迁移的cpu不在允许迁移的cpu集中,则不允许迁移
	// 这是肯定的,因为在前一个函数中env->cpus里存的只是激活状态的cpu
	if (!cpumask_test_cpu(env->dst_cpu, env->cpus))
		return 0;

	// 如果被迁移的cpu的idle状态是CPU_NEWLY_IDLE,则允许迁移
	// CPU_NEWLY_IDLE的意思应该是即将进入空闲状态的cpu,这个标志在代码里
	// 只有调度时,如果队列里没有任务时使用这个标志进行均衡
	if (env->idle == CPU_NEWLY_IDLE)
		return 1;

	// 走到这里,就不是从调度的idle进到的负载均衡,而是从时钟中断进入的,
	// 或者第一个cpu,如果找出的cpu不是当前的cpu,
	// 则不需要调度。

	// for_each_cpu_and是遍历第二,三个参数的交集
	// 在这里就是遍历调度组内的cpu,下面会找出组内第一个空闲的cpu
	for_each_cpu_and(cpu, group_balance_mask(sg), env->cpus) {
		if (!idle_cpu(cpu))
			continue;

		balance_cpu = cpu;
		break;
	}

	// 如果没有找到空闲cpu,则找出当前组内第一个cpu
	if (balance_cpu == -1)
		balance_cpu = group_balance_cpu(sg);

	// 只有第一个空闲CPU,或者第一个cpu才能做负载均衡
	return balance_cpu == env->dst_cpu;
}
// kernel/sched/fair.c

static struct sched_group *find_busiest_group(struct lb_env *env)
{
	struct sg_lb_stats *local, *busiest;
	struct sd_lb_stats sds;

	// 将sds中的变量都初始化成0
	init_sd_lb_stats(&sds);

	// 计算与负载均衡相关的信息,在这个函数里会更新调度域内
	// 各个组的负载,并选出最繁忙的一个组
	update_sd_lb_stats(env, &sds);
	local = &sds.local_stat;
	busiest = &sds.busiest_stat;

	// 不对称cpu打包。把低优先级cpu上的任务往高cpu上转移(一般cpu序号越低优先级越高)
	if (check_asym_packing(env, &sds))
		return sds.busiest;

	// 如果没有忙的调度组,或者最忙的调度组没有任务,
	// 那说明整个组都是空闲的
	if (!sds.busiest || busiest->sum_nr_running == 0)
		goto out_balanced;

	// 计算调度域的平均负载,SCHED_CAPACITY_SCALE = 1 << 10,
	// SCHED_CAPACITY_SCALE是一个比例因子,看不懂了可以忽略
	sds.avg_load = (SCHED_CAPACITY_SCALE * sds.total_load)
						/ sds.total_capacity;

	// 如果最繁忙的组类型为不平衡,则去强制平衡,不再做下面的检测,
	// 因为下面的检测假设组类型为平衡的或者过载
	if (busiest->group_type == group_imbalanced)
		goto force_balance;

	// 如果当前cpu空闲,且本地组里有容量,最忙组里没容量,则强制平衡
	if (env->idle != CPU_NOT_IDLE && group_has_capacity(env, local) &&
	    busiest->group_no_capacity)
		goto force_balance;

	// 如果当前组的平均负载比最忙的组还忙,则不进行负载均衡
	if (local->avg_load >= busiest->avg_load)
		goto out_balanced;

	// 如果当前组的平均负载比调度域内的平均负载高,则不进行负载均衡
	if (local->avg_load >= sds.avg_load)
		goto out_balanced;

	if (env->idle == CPU_IDLE) {
		// 如果最繁忙的组不是过载,并且本地的空闲cpu数与最繁忙的组空闲cpu差不多,
		// 则不进行负载均衡,这时如果进行均衡,很可能会将不平衡转移到另一个cpu上
		if ((busiest->group_type != group_overloaded) &&
				(local->idle_cpus <= (busiest->idle_cpus + 1)))
			goto out_balanced;
	} else {
		// 最忙组的平均负载小于本地组的平均负载,则不进行负载均衡
		if (100 * busiest->avg_load <=
				env->sd->imbalance_pct * local->avg_load)
			goto out_balanced;
	}

force_balance:
	// 计算需要移动的负载量,这个量就是要从最繁忙队列拉取任务的最大值
	calculate_imbalance(env, &sds);

	// 负载量不为0,则返回最繁忙的组,否则返回NULL
	return env->imbalance ? sds.busiest : NULL;

out_balanced:
	env->imbalance = 0;
	return NULL;
}

static inline void calculate_imbalance(struct lb_env *env, struct sd_lb_stats *sds)
{
	unsigned long max_pull, load_above_capacity = ~0UL;
	struct sg_lb_stats *local, *busiest;

	local = &sds->local_stat;
	busiest = &sds->busiest_stat;

	// 如果当前组类型为不平衡,则选取load_per_task和调用域avg_load
	// 的较小值作为最终的load_per_task
	if (busiest->group_type == group_imbalanced) {
		busiest->load_per_task =
			min(busiest->load_per_task, sds->avg_load);
	}

	// 如果最忙的组平均负载小于调度域的平均负载或者
	// 本地组的平均负载大于调度域的平均负载
	if (busiest->avg_load <= sds->avg_load ||
	    local->avg_load >= sds->avg_load) {
		env->imbalance = 0;
		return fix_small_imbalance(env, sds);
	}

	// 如果最忙的组和本地组的类型都是过载,则计算负载容量的上限
	// todo: 负载容量上限的计算没看懂
	if (busiest->group_type == group_overloaded &&
	    local->group_type   == group_overloaded) {
		load_above_capacity = busiest->sum_nr_running * SCHED_CAPACITY_SCALE;
		if (load_above_capacity > busiest->group_capacity) {
			load_above_capacity -= busiest->group_capacity;
			load_above_capacity *= scale_load_down(NICE_0_LOAD);
			load_above_capacity /= busiest->group_capacity;
		} else
			load_above_capacity = ~0UL;
	}

	/**
	  原文注释:我们尽量让所有cpu达到平均负载。所以在进行了负载均衡后,当前的cpu负载在平均
	  负载之后了,也不希望把最忙cpu的负载降低到平均负载之下。同时,我们也不想把组负载减少到组的容量
	  之下,因此我们寻找最小可能的平衡数量。
	*/
	// 通过原作者做的注释可以看出,负载均衡是想让所以组都达到平均负载,所以尽量减少要平衡的进程数量。	
	// 所以下面max_pull就是要拉取的最大负载,用最忙组的平均负载减去调度域内的平均负载,
	// 所以在理想情况下,可以让所有调度组达到平均负载
	max_pull = min(busiest->avg_load - sds->avg_load, load_above_capacity);

	// 上面算出了最忙组减多少负载可以平衡,这里则算的是本地组加多少负载可以平衡。
	// 最终需要拉取的负载为2者的较小值。
	// 肯定要选较小值,如果是选两者的较大值,有可能导致本地组超过调度域的平均负载,导致新的不平衡
	env->imbalance = min(
		max_pull * busiest->group_capacity,
		(sds->avg_load - local->avg_load) * local->group_capacity
	) / SCHED_CAPACITY_SCALE;

	// 如果要拉取的负载值小于最忙调度组的每个任务的平均负载,
	// 那有可能在后面连1个任务都拉取不到,所以这里就要处理这种情况,
	// 要保证要拉取的负载值,最少能拉到1个任务
	if (env->imbalance < busiest->load_per_task)
		return fix_small_imbalance(env, sds);
}

static inline
void fix_small_imbalance(struct lb_env *env, struct sd_lb_stats *sds)
{
	unsigned long tmp, capa_now = 0, capa_move = 0;
	unsigned int imbn = 2; // 移动任务的数量,默认移动2个
	unsigned long scaled_busy_load_per_task;
	struct sg_lb_stats *local, *busiest;

	local = &sds->local_stat;
	busiest = &sds->busiest_stat;

	if (!local->sum_nr_running)
		// 如果本组没有要运行的任务,则每个任务的平均负载,为目标cpu的平均负载
		local->load_per_task = cpu_avg_load_per_task(env->dst_cpu);
	else if (busiest->load_per_task > local->load_per_task)
		// 如果最忙组的任务负载大于本地组的任务负载,则只需要
		// 移动1个任务就够了
		imbn = 1;

	// 计算任务负载与组容量的比例
	scaled_busy_load_per_task =
		(busiest->load_per_task * SCHED_CAPACITY_SCALE) /
		busiest->group_capacity;

	// 如果给最繁忙的组再加上一个任务的负载大于本地组再加上1或2个任务的负载,
	// 则需要拉取的负载量为一个任务的负载值
	if (busiest->avg_load + scaled_busy_load_per_task >=
	    local->avg_load + (scaled_busy_load_per_task * imbn)) {
		env->imbalance = busiest->load_per_task;
		return;
	}

	// 走到这里就说明,没有足够的负载需要去调整。但是可以通过移动任务来减少总cpu的使用容量。
	// 这也是对cpu负载有好处的。

	// 先计算出来现在的负载
	capa_now += busiest->group_capacity *
			min(busiest->load_per_task, busiest->avg_load);
	capa_now += local->group_capacity *
			min(local->load_per_task, local->avg_load);
	capa_now /= SCHED_CAPACITY_SCALE;

	// 再计算要移动的负载量
	if (busiest->avg_load > scaled_busy_load_per_task) {
		capa_move += busiest->group_capacity *
			    min(busiest->load_per_task,
				busiest->avg_load - scaled_busy_load_per_task);
	}

	// 每个平均负载的值
	if (busiest->avg_load * busiest->group_capacity <
	    busiest->load_per_task * SCHED_CAPACITY_SCALE) {
		tmp = (busiest->avg_load * busiest->group_capacity) /
		      local->group_capacity;
	} else {
		tmp = (busiest->load_per_task * SCHED_CAPACITY_SCALE) /
		      local->group_capacity;
	}
	capa_move += local->group_capacity *
		    min(local->load_per_task, local->avg_load + tmp);
	capa_move /= SCHED_CAPACITY_SCALE;

	// 可以移动的负载大于当前的负载,则设置移动量为一个task的负载
	if (capa_move > capa_now)
		env->imbalance = busiest->load_per_task;
}
static inline void update_sd_lb_stats(struct lb_env *env, struct sd_lb_stats *sds)
{
	struct sched_domain *child = env->sd->child; // 子调度域
	struct sched_group *sg = env->sd->groups; // 该调度域内的组
	struct sg_lb_stats *local = &sds->local_stat; // 本地统计信息
	struct sg_lb_stats tmp_sgs;
	int load_idx, prefer_sibling = 0;
	bool overload = false; // 是否过载

	// 根据字面意思,如果子调度域喜欢兄弟调度域,则更倾向在这些调度之间平衡负载
	if (child && child->flags & SD_PREFER_SIBLING)
		prefer_sibling = 1;

#ifdef CONFIG_NO_HZ_COMMON
	if (env->idle == CPU_NEWLY_IDLE && READ_ONCE(nohz.has_blocked))
		env->flags |= LBF_NOHZ_STATS;
#endif

	// 根据idle状态,来获取该调度域内的相应cpu的索引值
	// todo: 没看懂,这个值在初始化的时候是0
	load_idx = get_sd_load_idx(env->sd, env->idle);

	do {
		struct sg_lb_stats *sgs = &tmp_sgs;
		int local_group;

		// 判断目标cpu是不是当前调度组的
		local_group = cpumask_test_cpu(env->dst_cpu, sched_group_span(sg));
		if (local_group) {
			// 如果是当前调度组的,则记录当前调度组为本地组
			sds->local = sg;
			sgs = local;

			// 如果目标cpu不是即将进入空闲的cpu,或者当前时间已经超过了调度组下次更新的时间,
			// 则更新调度组的容量。
			// todo:这里为什么只在目标cpu属于当前调度组的时候更新调度组容量呢?
			if (env->idle != CPU_NEWLY_IDLE ||
			    time_after_eq(jiffies, sg->sgc->next_update))
				update_group_capacity(env->sd, env->dst_cpu);
		}

		// 更新调度组的负载统计数据
		update_sg_lb_stats(env, sg, load_idx, local_group, sgs,
						&overload);

		// 如果是本地组的话,继续遍历下个调度组,
		// 这里就不用将本地组记录为繁忙组了,因为这里找的是最忙组,
		// 如果本组是最忙的,就不用再做负载均衡了
		if (local_group)
			goto next_group;

		// todo: 没看懂
		if (prefer_sibling && sds->local &&
		    group_has_capacity(env, local) &&
		    (sgs->sum_nr_running > local->sum_nr_running + 1)) {
			sgs->group_no_capacity = 1;
			sgs->group_type = group_classify(sg, sgs);
		}

		// 比较当前调度组与老的繁忙组的统计数据,看当前调度组是不是最忙的,
		// 如果是的话,记录之。
		if (update_sd_pick_busiest(env, sds, sg, sgs)) {
			sds->busiest = sg;
			sds->busiest_stat = *sgs;
		}

next_group:
		// 更新调度域里的总运行任务数
		sds->total_running += sgs->sum_nr_running;

		// 更新调度域里的总负载
		sds->total_load += sgs->group_load;

		// 更新调度域里的总容量
		sds->total_capacity += sgs->group_capacity;

		sg = sg->next;
	} while (sg != env->sd->groups);

#ifdef CONFIG_NO_HZ_COMMON
	// NO_HZ相关处理
	// todo: 没看懂
	if ((env->flags & LBF_NOHZ_AGAIN) &&
	    cpumask_subset(nohz.idle_cpus_mask, sched_domain_span(env->sd))) {

		WRITE_ONCE(nohz.next_blocked,
			   jiffies + msecs_to_jiffies(LOAD_AVG_PERIOD));
	}
#endif

	// todo: 没看懂,好像是在判断当前环境中有无远程numa结点
	if (env->sd->flags & SD_NUMA)
		env->fbq_type = fbq_classify_group(&sds->busiest_stat);

	// 如果当前cpu的调度域是根调度域,则更新它的过载标志
	if (!env->sd->parent) {
		if (env->dst_rq->rd->overload != overload)
			env->dst_rq->rd->overload = overload;
	}
}

static bool update_sd_pick_busiest(struct lb_env *env,
				   struct sd_lb_stats *sds,
				   struct sched_group *sg,
				   struct sg_lb_stats *sgs)
{
	/**
	 在init_sd_lb_stats中,busiest_stat被初始化成下面这样,
	 .busiest_stat = {
			.avg_load = 0UL,
			.sum_nr_running = 0,
			.group_type = group_other,
		},
	*/
	struct sg_lb_stats *busiest = &sds->busiest_stat;

	/**
		group_type定义如下,值越大表示越繁忙。
		enum group_type {
			group_other = 0,
			group_imbalanced,
			group_overloaded,
		};
	*/
	if (sgs->group_type > busiest->group_type)
		return true;

	if (sgs->group_type < busiest->group_type)
		return false;

	// 比较平均负载
	if (sgs->avg_load <= busiest->avg_load)
		return false;

	// 走到这里,说明sgs和busiest的group_type相同,
	// 而且平均负载也比busiest的大
	
	// todo: 走到这里已经判断出sg的平均负载比最忙负载大,
	// 为啥不直接返回true


	// SD_ASYM_CPUCAPACITY是cpu算力不对称的标志
	// 也就是说如果这个调度域里的算力都是对称的,直接跳到asym_packing处
	if (!(env->sd->flags & SD_ASYM_CPUCAPACITY))
		goto asym_packing;

	// todo: 没看明白
	if (sgs->sum_nr_running <= sgs->group_weight &&
	    group_smaller_cpu_capacity(sds->local, sg))
		return false;

asym_packing:
	// 如果本调度域都是对称打包,这个sg就是最忙的组
	if (!(env->sd->flags & SD_ASYM_PACKING))
		return true;

	// 如果当前cpu不空闲,说明比busiest忙
	if (env->idle == CPU_NOT_IDLE)
		return true;

	// 走到这里是cpu空闲时,也就是队列里没有任务

	
	// sched_asym_prefer是比较第1个和第2个cpu哪个优化级高
	// 在不对称算力的处理器中一般都是序号小的处理器算力大
	
	// 如果要均衡的cpu算力比sg组内的高优先级的cpu还高
	if (sgs->sum_nr_running &&
	    sched_asym_prefer(env->dst_cpu, sg->asym_prefer_cpu)) {
		if (!sds->busiest)
			return true;

		// 更愿意从低优先级的cpu上转移进程
		if (sched_asym_prefer(sds->busiest->asym_prefer_cpu,
				      sg->asym_prefer_cpu))
			return true;
	}

	return false;
}

void update_group_capacity(struct sched_domain *sd, int cpu)
{
	struct sched_domain *child = sd->child; // 子调度域
	struct sched_group *group, *sdg = sd->groups; // 该调度域内的调度组
	unsigned long capacity, min_capacity;
	unsigned long interval;

	// 计算下次进行更新的间隔,
	// 可以看出下次更新的间隔为sd->balance_interval,但是这个值被限制
	// 在 1UL~max_load_balance_interval之间,
	// 

	// 下次更新的间隔为sd->balance_interval
	interval = msecs_to_jiffies(sd->balance_interval);

	// 间隔值被限制在 1UL~max_load_balance_interval之间,
	// max_load_balance_interval = HZ*num_online_cpus()/10;
	// 可以看出max_load_balance_interval与在线cpu有关
	interval = clamp(interval, 1UL, max_load_balance_interval);

	// 更新下次更新的时间点
	sdg->sgc->next_update = jiffies + interval;

	// 如果没有子调度域,说明这是叶子结点,所以直接更新调度域内的cpu容量
	if (!child) {
		update_cpu_capacity(sd, cpu);
		return;
	}

	capacity = 0;
	min_capacity = ULONG_MAX;

	// 子调度域有重叠,todo: 何为重叠呢?
	if (child->flags & SD_OVERLAP) {
		/*
		 * SD_OVERLAP domains cannot assume that child groups
		 * span the current group.
		 */

		for_each_cpu(cpu, sched_group_span(sdg)) {
			struct sched_group_capacity *sgc;
			struct rq *rq = cpu_rq(cpu);

			// 如果cpu的队列没有调度域,则直接加cpu的容量
			if (unlikely(!rq->sd)) {
				capacity += capacity_of(cpu);
			} else {
				sgc = rq->sd->groups->sgc;
				capacity += sgc->capacity;
			}

			min_capacity = min(capacity, min_capacity);
		}
	} else  {
		// 如果没有重叠的话,则统计所有调度组的容量,
		// 以及记录调度组的cpu的最小容量
		group = child->groups;
		do {
			struct sched_group_capacity *sgc = group->sgc;

			capacity += sgc->capacity;
			min_capacity = min(sgc->min_capacity, min_capacity);
			group = group->next;
		} while (group != child->groups);
	}

	// 更新调组的容量值
	sdg->sgc->capacity = capacity;
	sdg->sgc->min_capacity = min_capacity;
}

static void update_cpu_capacity(struct sched_domain *sd, int cpu)
{
	// 计算cpu容量, 
	// 容量 = 当前cpu最大容量值 - 已使用的容量
	unsigned long capacity = scale_rt_capacity(sd, cpu);
	struct sched_group *sdg = sd->groups;

	// arch_scale_cpu_capacity算出来的是当前cpu最大容量值
	cpu_rq(cpu)->cpu_capacity_orig = arch_scale_cpu_capacity(sd, cpu);

	if (!capacity)
		capacity = 1;

	// 更新cpu容量
	cpu_rq(cpu)->cpu_capacity = capacity;

	// 更新调度组容量,因为当前组只有一个cpu,所以
	// 该调度组的容量与cpu的容量相同
	sdg->sgc->capacity = capacity;
	sdg->sgc->min_capacity = capacity;
}
static inline void update_sg_lb_stats(struct lb_env *env,
			struct sched_group *group, int load_idx,
			int local_group, struct sg_lb_stats *sgs,
			bool *overload)
{
	unsigned long load;
	int i, nr_running;

	// sgs是组的负载统计数据,在更新的时候先将它清0
	memset(sgs, 0, sizeof(*sgs));

	// 遍历组内cpu
	for_each_cpu_and(i, sched_group_span(group), env->cpus) {
		struct rq *rq = cpu_rq(i);

		// todo: 没看懂
		if ((env->flags & LBF_NOHZ_STATS) && update_nohz_stats(rq, false))
			env->flags |= LBF_NOHZ_AGAIN;

		// 根据是否是组内cpu,来计算当前cpu的负载
		// target_load和source_load的计算过程一模一样,只不过
		// target_load返回的是rq->cpu_load[load_idx-1],和cpu负载的较大值,
		// source_load返回的是较小值。
		// 这个load返回的是该队列的的平均负载值,这个平均负载值是根据pelt计算
		// 这里的load负载包含了队列中的负载和运行时的负载,pelt算法的作者认为,即使
		// 没有在cpu上运行,该task同样会对系统产生负载
		if (local_group)
			load = target_load(i, load_idx);
		else
			load = source_load(i, load_idx);

		// 统计每个cpu的负载
		sgs->group_load += load;

		// 统计每个cpu正在运行时的负载
		// util负载是只在cpu上运行的负载
		sgs->group_util += cpu_util(i);

		// 统计每个cpu的任务数量
		// h_nr_running好像是控制组内的所有进程数量,如果是普通task,
		// 则这个值和nr_running的值相同
		sgs->sum_nr_running += rq->cfs.h_nr_running;

		// 当前cpu有超过一个任务时就认为是过载的,
		// todo:这里没看懂,只有一个任务就过载,那是不是只有空队列才是不过载的?
		nr_running = rq->nr_running;
		if (nr_running > 1)
			*overload = true;

#ifdef CONFIG_NUMA_BALANCING
		// 如果配置了numa平衡,则统计numa相关的信息
		// 这个配置选项,会自动迁移任务的内存到最近的numa结点
		
		// nr_numa_running:设置了numa节点的进程数量
		// nr_preferred_running:当前numa节点与进程想要运行的numa节点相同的进程数量
		sgs->nr_numa_running += rq->nr_numa_running;
		sgs->nr_preferred_running += rq->nr_preferred_running;
#endif
		// 统计总权重负载,其实上面的load也是用weighted_cpuload算出来的
		sgs->sum_weighted_load += weighted_cpuload(rq);
		
		// 如果当前cpu是空闲,则增加组内的空闲cpu计数器
		if (!nr_running && idle_cpu(i))
			sgs->idle_cpus++;
	}

	// 统计组容量
	sgs->group_capacity = group->sgc->capacity;

	// 统计组平均负载,SCHED_CAPACITY_SCALE=1UL << 10
	// 平均负载=组负载/组容量。todo: 为什么要*SCHED_CAPACITY_SCALE?
	sgs->avg_load = (sgs->group_load*SCHED_CAPACITY_SCALE) / sgs->group_capacity;

	// 计算每个task的负载
	// 每个task的负载 = 总负载 / 运行的进程数。这里的运行包括在队列里和正在cpu上运行的
	if (sgs->sum_nr_running)
		sgs->load_per_task = sgs->sum_weighted_load / sgs->sum_nr_running;

	// 统计组权重
	sgs->group_weight = group->group_weight;

	// 计算调度组是不是没容量了
	sgs->group_no_capacity = group_is_overloaded(env, sgs);

	// 调整组的类型
	// 如果group_no_capacity,则返回group_overloaded,过载
	// 如果sgc->imbalance为真,则返回group_imbalanced,不平衡
	// 否则就是group_other。group_other应该是正常的情况
	// 不平衡说明快要超出负载承受范围了,过载说明已经明显超过负载(todo: 猜的,不是很肯定)
	sgs->group_type = group_classify(group, sgs);
}

static inline bool
group_is_overloaded(struct lb_env *env, struct sg_lb_stats *sgs)
{
	// 如果组内运行任务的数量比组权重小,那肯定是没有过载
	if (sgs->sum_nr_running <= sgs->group_weight)
		return false;

	// todo: 没看懂。
	if ((sgs->group_capacity * 100) <
			(sgs->group_util * env->sd->imbalance_pct))
		return true;

	return false;
}

static unsigned long weighted_cpuload(struct rq *rq)
{
	return cfs_rq_runnable_load_avg(&rq->cfs);
}

static unsigned long source_load(int cpu, int type)
{
	struct rq *rq = cpu_rq(cpu);
	unsigned long total = weighted_cpuload(rq);

	if (type == 0 || !sched_feat(LB_BIAS))
		return total;

	return min(rq->cpu_load[type-1], total);
}

// kernel/sched/fair.c

static struct rq *find_busiest_queue(struct lb_env *env,
				     struct sched_group *group)
{
	struct rq *busiest = NULL, *rq;
	unsigned long busiest_load = 0, busiest_capacity = 1;
	int i;

	// 遍历调度组内的cpu,取的是第2,3参数交集的cpu
	for_each_cpu_and(i, sched_group_span(group), env->cpus) {
		unsigned long capacity, wl;

		// enum fbq_type { regular, remote, all };
		// 这三个枚举的定义见下面英文注释
		enum fbq_type rt;

		// 运行队列
		rq = cpu_rq(i);

		// 队列类型,类型见下面注释的定义,有numa和非numa之分
		rt = fbq_classify_rq(rq);

		/*
		 * We classify groups/runqueues into three groups:
		 *  - regular: there are !numa tasks
		 *  - remote:  there are numa tasks that run on the 'wrong' node
		 *  - all:     there is no distinction
		 *
		 * In order to avoid migrating ideally placed numa tasks,
		 * ignore those when there's better options.
		 *
		 * If we ignore the actual busiest queue to migrate another
		 * task, the next balance pass can still reduce the busiest
		 * queue by moving tasks around inside the node.
		 *
		 * If we cannot move enough load due to this classification
		 * the next pass will adjust the group classification and
		 * allow migration of more tasks.
		 *
		 * Both cases only affect the total convergence complexity.
		 */
		 /**
			regular: 表示当前队列里没有numa的任务
			remote: 表示当前队列里有numa的任务。有numa任务意味着有任务要访问远程numa节点
			all: 表示当前队列不区别是否有numa任务

			如果一个队列里有numa任务,那这个task对内存的访问肯定比较慢
		 */
		 // todo:没看懂
		if (rt > env->fbq_type)
			continue;

		// 运行队列容量
		// 容量最大是1024,空闲容量是用最大容量减去使用的容量
		capacity = capacity_of(i);

		// 队列运行时平均负载,这个负载包括了可运行和就绪时
		wl = weighted_cpuload(rq);

		// 如果当前队列只有一个任务,而且队列的负载大于需要pull的负载值时,
		// 如果移动这个队列的任务,则会使这个cpu成为空闲cpu
		// 而且移动过去的负载大于需要移动的负载会造成新的不平衡,所以忽略之
		if (rq->nr_running == 1 && wl > env->imbalance &&
		    !check_cpu_capacity(rq, env->sd))
			continue;

		// 记录最繁忙队列的的信息
		// todo: 为什么比较权重*容量
		if (wl * busiest_capacity > busiest_load * capacity) {
			busiest_load = wl;
			busiest_capacity = capacity;
			busiest = rq;
		}
	}

	return busiest;
}

cpu队列为空

在调度的时候要选择一个新任务,选择新任务是交给每个调度器去选择:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	const struct sched_class *class;
	struct task_struct *p;

	// 这里做了一个优化,因为大多数进程的调度器都是fair_sched_class,
	// 所以这里如果是fair或者idle时,就不用遍历每个调度器了
	if (likely((prev->sched_class == &idle_sched_class ||
		    prev->sched_class == &fair_sched_class) &&
		   rq->nr_running == rq->cfs.h_nr_running)) {

		// 调用具体调度器去选择任务
		p = fair_sched_class.pick_next_task(rq, prev, rf);

		// RETRY_TASK是迁移任务失败,所以就跳到again,遍历每个调度器
		// RETRY_TASK的定义是((void*)-1UL)
		if (unlikely(p == RETRY_TASK))
			goto again;

		// 如果返回NULL,则直接调用idle调度器类
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev, rf);

		return p;
	}

again:
	// 这里要遍历每个调度器去选择新task,for_each_class的定义在下面
	for_each_class(class) {
		p = class->pick_next_task(rq, prev, rf);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

	// 正常情况下是不可能走到这的,即使系统里一个进程也没有,也有idle进程
	// 所以走到这,肯定是bug
	BUG();
}

// kernel/sched/sched.h

// 从for_each_class的定义可以看出,头结点是stop_sched_class,各调度器
// 的链接关系为:stop_sched_class->dl_sched_class->rt_sched_class
// ->fair_sched_class->idle_sched_class->NULL
#ifdef CONFIG_SMP
#define sched_class_highest (&stop_sched_class)
#else
#define sched_class_highest (&dl_sched_class)
#endif
#define for_each_class(class) \
   for (class = sched_class_highest; class; class = class->next)
// kernel/sched/fair.c

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	struct cfs_rq *cfs_rq = &rq->cfs;
	struct sched_entity *se;
	struct task_struct *p;
	int new_tasks;

again:
	// 如果运行队列为空,则跳到idle进行负载均衡
	if (!cfs_rq->nr_running)
		goto idle;

...

idle:

	// 进行负载均衡,返回的new_tasks是转移到本队列的数量
	new_tasks = idle_balance(rq, rf);

	// 如果有高优先级的任务,则返回上层函数,遍历调度器类
	if (new_tasks < 0)
		return RETRY_TASK;

	// 如果迁移到了任务,则跳到again,选择新任务
	if (new_tasks > 0)
		goto again;

	// 如果没有迁移到任务,则返回上层,会运行idle任务
	return NULL;
}
// kernel/sched/fair.c

static int idle_balance(struct rq *this_rq, struct rq_flags *rf)
{
	unsigned long next_balance = jiffies + HZ;
	int this_cpu = this_rq->cpu;
	struct sched_domain *sd;
	int pulled_task = 0;
	u64 curr_cost = 0; // 迁移花费的总时间

	// 记录idle的时间戳
	this_rq->idle_stamp = rq_clock(this_rq);

	// 如果当前cpu不是活跃的,当然不能给它迁移任务
	// todo: 不知道在哪设置的这个状态
	if (!cpu_active(this_cpu))
		return 0;

	/*
	 * This is OK, because current is on_cpu, which avoids it being picked
	 * for load-balance and preemption/IRQs are still disabled avoiding
	 * further scheduler activity on it and we're being very careful to
	 * re-start the picking loop.
	 */
	rq_unpin_lock(this_rq, rf);

	// 如果当前队列的平均空闲时间小于迁移花费的时间,或者当前队列没有过载,则不进行迁移
	// 空闲平均时间是从cpu空间时记下一个时间戳,等到唤醒时再计算空新闲时长
	// 这时如果迁移的话划不来,可能会造成新的不平衡
	if (this_rq->avg_idle < sysctl_sched_migration_cost ||
	    !this_rq->rd->overload) {

		rcu_read_lock();
		sd = rcu_dereference_check_sched_domain(this_rq->sd);

		// 更新sd的下一次平衡的时间
		if (sd)
			update_next_balance(sd, &next_balance);
		rcu_read_unlock();

		nohz_newidle_balance(this_rq);

		goto out;
	}

	raw_spin_unlock(&this_rq->lock);

	// todo: update_blocked_averages没看懂,应该是更新平均负载相关的
	update_blocked_averages(this_cpu);
	rcu_read_lock();

	// 遍历所有调度域
	for_each_domain(this_cpu, sd) {
		int continue_balancing = 1; // 是否要继续平衡
		u64 t0, domain_cost; // 域迁移花费的时间

		// 如果当前调度域不允许进行负载均衡,则跳过
		if (!(sd->flags & SD_LOAD_BALANCE))
			continue;

		// 如果当前队列的空间时间小于迁移花费和空闲迁移花费的和,则不再迁移,结束循环
		if (this_rq->avg_idle < curr_cost + sd->max_newidle_lb_cost) {
			update_next_balance(sd, &next_balance);
			break;
		}

		// 如果当前调度域有SD_BALANCE_NEWIDLE这个标志时,则进行真正的迁移
		// NEWIDLE是当前cpu即将进入空闲时的标志,因为这个方法只有这种情景下才会
		// 调用,所以这里在选择调度域时也要选有空闲平衡标志的调度域
		if (sd->flags & SD_BALANCE_NEWIDLE) {
			// 开始时间
			t0 = sched_clock_cpu(this_cpu);

			// 进行负载平衡,pulled_task为拉的进程数量,负载均衡有2种模式:pull和push,
			// 前者是主动从其他cpu往本队列迁移任务,push是当前cpu往其他cpu推任务
			pulled_task = load_balance(this_cpu, this_rq,
						   sd, CPU_NEWLY_IDLE,
						   &continue_balancing);

			// 计算本次迁移的花费
			domain_cost = sched_clock_cpu(this_cpu) - t0;

			// 更新调度域的max_newidle_lb_cost
			if (domain_cost > sd->max_newidle_lb_cost)
				sd->max_newidle_lb_cost = domain_cost;

			// 更新当前迁移花费的总时间
			curr_cost += domain_cost;
		}

		// 更新下次再进行负载均衡的时间
		update_next_balance(sd, &next_balance);

		// 如果已经拉到了任务,或者当前队列已经有了可运行的任务,
		// 则不再进行负载均衡
		if (pulled_task || this_rq->nr_running > 0)
			break;
	}
	rcu_read_unlock();

	raw_spin_lock(&this_rq->lock);

	// 更新当前cpu本次迁移的花费时间
	if (curr_cost > this_rq->max_idle_balance_cost)
		this_rq->max_idle_balance_cost = curr_cost;

out:
	// 如果当前调度组内有了可以运行的任务,即使没有拉到任务也返回1
	if (this_rq->cfs.h_nr_running && !pulled_task)
		pulled_task = 1;

	// 更新当前队列下次负载平衡的时间
	if (time_after(this_rq->next_balance, next_balance))
		this_rq->next_balance = next_balance;

	// 如果有高优先级的任务,需要遍历调度器链选择高优先级的任务来调度
	if (this_rq->nr_running != this_rq->cfs.h_nr_running)
		pulled_task = -1;

	// 如果拉到了任务,则重置空闲时间戳
	if (pulled_task)
		this_rq->idle_stamp = 0;

	rq_repin_lock(this_rq, rf);

	return pulled_task;
}

select_task_rq

kernel/sched/core.c

/**
这个函数主要是在唤醒一个进程时来进行负载均衡

p: 选择cpu的进程
cpu: 进程当前所在cpu
sd_flags: 调度域标志。需要目标cpu有这个标志
wake_flags: 唤醒的标志。是在什么情况下唤醒的

*/
static inline
int select_task_rq(struct task_struct *p, int cpu, int sd_flags, int wake_flags)
{
	// 判断是否持有pi_lock这个锁
	// todo: 什么意思?
	lockdep_assert_held(&p->pi_lock);

	if (p->nr_cpus_allowed > 1)
		// 当进程允许的cpu大于1个时,调用调度器类的select_task_rq,来挑选一个cpu(运行队列)
		cpu = p->sched_class->select_task_rq(p, cpu, sd_flags, wake_flags);
	else
		// cpumask_any是从cpus_allowed中选择第一个cpu,
		// 如果找不到cpu,则会返回cpu个数的最大值
		cpu = cpumask_any(&p->cpus_allowed);

	// 如果上面选的cpu都用不了,则重新在调度域里选一个cpu
	if (unlikely(!is_cpu_allowed(p, cpu)))
		// 如果上面选择的cpu不是进程所允许的cpu,则重新选择一个合适的cpu
		cpu = select_fallback_rq(task_cpu(p), p);

	return cpu;
}
kernel/sched/fair.c

static int
select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
{
	struct sched_domain *tmp, *sd = NULL;
	int cpu = smp_processor_id(); // 当前cpu
	int new_cpu = prev_cpu; // 进程上一次运行的cpu
	int want_affine = 0;

	// WF_SYNC表示:这个进程在唤醒之后很快就会再次调度离开,
	// 所以要避免迁移进程,防止在cpu之间来回跳转。
	int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);

	// SD_BALANCE_WAKE只在try_to_wake_up的时候使用过,
	// 表示当前是从唤醒进来的
	if (sd_flag & SD_BALANCE_WAKE) {
		// 记录唤醒的进程次数与被唤醒的进程
		record_wakee(p);

		// wake_wide判断唤醒次数是否太多,wake_cap判断当前cpu是否有能力承载进程p

		// 如果唤醒其它进程次数不太多,当前cpu的可用算力能够承载进程p,且当前cpu
		// 在进程p的允许运行cpu位图中,则置亲和性标志
		want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu)
			      && cpumask_test_cpu(cpu, &p->cpus_allowed);
	}

	rcu_read_lock();

	// 自底向上遍历调度域
	// 这里的自底是从cpu当前所在的最底调度域向上遍历
	for_each_domain(cpu, tmp) {

		// 如果当前调度域不支持负载均衡则直接退出循环
		// todo: 为什么这里就直接退出循环了,其它的两个负载均衡
		// 在当前调度域不支持均衡的时候,会继续在上层均衡
		if (!(tmp->flags & SD_LOAD_BALANCE))
			break;

		// 如果亲和性标志为1,当前调度域支持SD_WAKE_AFFINE,
		// 并且之前运行的cpu,属于当前调度域
		// 这个条件应该大多数情况下都能成立
		if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&
		    cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
			if (cpu != prev_cpu)
				// 选择一个亲和性cpu
				// 这里应该是大概率选择当前的cpu
				new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);

			sd = NULL; /* Prefer wake_affine over balance flags */
			break;
		}

		if (tmp->flags & sd_flag)
			sd = tmp;
		else if (!want_affine)
			break;
	}

	if (unlikely(sd)) {
		// 慢速路径

		// 这个条件就是没有找到有亲和性的cpu,
		// 如果找到了cpu上面的循环会把sd置NULL

		// find_idlest_cpu是从sd这个调度域里自上而下找一个空闲的cpu,
		// 因为前面的循环是自下而上的循环,最坏的结果就是sd为最项层的调度域
		new_cpu = find_idlest_cpu(sd, p, cpu, prev_cpu, sd_flag);
	} else if (sd_flag & SD_BALANCE_WAKE) {
		// 快速路径
		// SD_BALANCE_WAKE表示在唤醒的时候进行负载均衡

		// 走到这里意味着选择的cpu不是当前cpu就是以前的cpu
		// todo: 这里为啥还要通过select_idle_sibling选一把
		new_cpu = select_idle_sibling(p, prev_cpu, new_cpu);

		// 如果有亲各性标志,则记录最近使用的cpu
		if (want_affine)
			current->recent_used_cpu = cpu;
	}
	rcu_read_unlock();

	return new_cpu;
}

static void record_wakee(struct task_struct *p)
{
	// 如果当前时间与上次衰减的时候经过了1秒,
	// 则把唤醒不同进程的次数除以2, 再记录当前衰减的时间戳
	if (time_after(jiffies, current->wakee_flip_decay_ts + HZ)) {
		current->wakee_flips >>= 1;
		current->wakee_flip_decay_ts = jiffies;
	}

	// 记录当前进程上次唤醒的进程,和唤醒不同进程的次数
	if (current->last_wakee != p) {
		current->last_wakee = p;
		current->wakee_flips++;
	}
}

// 判断cpu的算力能否承载进程p
static int wake_cap(struct task_struct *p, int cpu, int prev_cpu)
{
	long min_cap, max_cap;

	// 两个cpu的实际算力
	min_cap = min(capacity_orig_of(prev_cpu), capacity_orig_of(cpu));

	// cpu的最大算力
	max_cap = cpu_rq(cpu)->rd->max_cpu_capacity;

	// 如果当前cpu的最大算力与最小算力之间相差不超过最大算力的1/3,
	// 则不用禁用wake的亲和性
	if (max_cap - min_cap < max_cap >> 3)
		return 0;

	/* Bring task utilization in sync with prev_cpu */
	sync_entity_load_avg(&p->se);

	// 判断最小算力的cpu是否能承载p进程
	return min_cap * 1024 < task_util(p) * capacity_margin;
}

// 判断p和master之间唤醒不同进程的次数是否太多
static int wake_wide(struct task_struct *p)
{
	unsigned int master = current->wakee_flips;
	unsigned int slave = p->wakee_flips;

	// sd_llc_size是最高级调度域共享缓存的cpu数目
	int factor = this_cpu_read(sd_llc_size);

	if (master < slave)
		swap(master, slave);
	if (slave < factor || master < slave * factor)
		return 0;
	return 1;
}

// 这个函数要么选this_cpu, 要么选prev_cpu,或者没找到
static int wake_affine(struct sched_domain *sd, struct task_struct *p,
		       int this_cpu, int prev_cpu, int sync)
{
	// target被初始化成最大cpu数目
	int target = nr_cpumask_bits;

	// WA_IDLE默认为true
	// 先找亲和性cpu
	if (sched_feat(WA_IDLE))
		target = wake_affine_idle(this_cpu, prev_cpu, sync);

	// WA_WEIGHT默认为true
	// wake_affine_weight根据负载情况,判断是否要返回this_cpu,
	// 如果负载不好,则返回最大cpu号,也就是没找到
	if (sched_feat(WA_WEIGHT) && target == nr_cpumask_bits)
		target = wake_affine_weight(sd, p, this_cpu, prev_cpu, sync);

	// 增加亲和性尝试统计次数
	schedstat_inc(p->se.statistics.nr_wakeups_affine_attempts);

	// 如果在上面还是没找到cpu,则返回之前运行的cpu
	if (target == nr_cpumask_bits)
		return prev_cpu;

	// 增加调度域亲和性迁移次数
	schedstat_inc(sd->ttwu_move_affine);

	// 增加进程亲和性迁移统计次数
	schedstat_inc(p->se.statistics.nr_wakeups_affine);
	return target;
}

// 找到亲和性的空闲cpu
static int wake_affine_idle(int this_cpu, int prev_cpu, int sync)
{
	/**
	原文注释:
		如果 this_cpu 空闲,则意味着唤醒来自中断上下文。 
		仅在共享缓存时才允许移动。 否则,中断密集型工作负
		载可能会根据 IO 拓扑或 IRQ 关联设置将所有任务强
		制到一个节点上。 如果 prev_cpu 空闲并且缓存亲和,
		则避免迁移。 不能保证来自中断的缓存热数据比 
		prev_cpu 上的缓存热数据更重要,从 cpufreq 的角
		度来看,最好在一个 CPU 上具有更高的利用率。
	*/
	// 和之前的cpu可以共享缓存,说明这两个cpu肯定在同一个物理cpu上
	if (available_idle_cpu(this_cpu) && cpus_share_cache(this_cpu, prev_cpu))
		// 调度系统会尽量使用之前运行的cpu来运行进程
		return available_idle_cpu(prev_cpu) ? prev_cpu : this_cpu;

	// 如果进程是同步的,并且当前cpu只有一个进程,则返回当前cpu
	if (sync && cpu_rq(this_cpu)->nr_running == 1)
		return this_cpu;

	// 否则返回cpu数目最大值,也就是没找到
	return nr_cpumask_bits;
}

// 如果当前cpu的负载小于之前cpu的负载,则返回当前cpu,否则返回最大cpu号
// todo: 计算过程没仔细看
static int wake_affine_weight(struct sched_domain *sd, struct task_struct *p,
		   int this_cpu, int prev_cpu, int sync)
{
	s64 this_eff_load, prev_eff_load;
	unsigned long task_load;

	this_eff_load = target_load(this_cpu, sd->wake_idx);

	if (sync) {
		// 如果是同步的,当前进程负载如果大于当前cpu的wake负载,
		// 则直接返回当前cpu
		unsigned long current_load = task_h_load(current);

		if (current_load > this_eff_load)
			return this_cpu;

		this_eff_load -= current_load;
	}

	// 当前进程的负载
	task_load = task_h_load(p);

	this_eff_load += task_load;

	// WA_BIAS标志默认为true
	if (sched_feat(WA_BIAS))
		this_eff_load *= 100;
	this_eff_load *= capacity_of(prev_cpu);

	// 之前cpu的负载
	prev_eff_load = source_load(prev_cpu, sd->wake_idx);
	prev_eff_load -= task_load;
	if (sched_feat(WA_BIAS))
		prev_eff_load *= 100 + (sd->imbalance_pct - 100) / 2;
	prev_eff_load *= capacity_of(this_cpu);

	/*
	 * If sync, adjust the weight of prev_eff_load such that if
	 * prev_eff == this_eff that select_idle_sibling() will consider
	 * stacking the wakee on top of the waker if no other CPU is
	 * idle.
	 */
	if (sync)
		prev_eff_load += 1;

	return this_eff_load < prev_eff_load ? this_cpu : nr_cpumask_bits;
}

static inline int find_idlest_cpu(struct sched_domain *sd, struct task_struct *p,
				  int cpu, int prev_cpu, int sd_flag)
{
	int new_cpu = cpu;

	// 如果进程允许的cpu和调度域里的cpu没有交集,则返回之前运行的cpu
	if (!cpumask_intersects(sched_domain_span(sd), &p->cpus_allowed))
		return prev_cpu;

	// 如果不是从fork进来的,则同步进程的平均负载
	if (!(sd_flag & SD_BALANCE_FORK))
		sync_entity_load_avg(&p->se);

	// 自上而下遍历调度域,直到找到最低层的空闲可运行cpu
	while (sd) {
		struct sched_group *group;
		struct sched_domain *tmp;
		int weight;

		// 如果当前调度域没有需要的标志,则遍历子域
		if (!(sd->flags & sd_flag)) {
			sd = sd->child;
			continue;
		}

		// 找最空闲的调度组
		group = find_idlest_group(sd, p, cpu, sd_flag);
		if (!group) {
			sd = sd->child;
			continue;
		}

		// 在最空闲的调度组里找最空闲的cpu
		new_cpu = find_idlest_group_cpu(group, p, cpu);
		if (new_cpu == cpu) {
			// 如果找到的新cpu和现在cpu相同,则继续在子域里找
			sd = sd->child;
			continue;
		}

		// 然后在低一层的调度域里找权重较小的调度域再次找空闲的cpu
		cpu = new_cpu;
		weight = sd->span_weight;
		sd = NULL;
		for_each_domain(cpu, tmp) {
			if (weight <= tmp->span_weight)
				break;
			if (tmp->flags & sd_flag)
				sd = tmp;
		}
	}

	return new_cpu;
}

/*
返回调度域内最空闲的调度组
 */
static struct sched_group *
find_idlest_group(struct sched_domain *sd, struct task_struct *p,
		  int this_cpu, int sd_flag)
{
	struct sched_group *idlest = NULL, *group = sd->groups;
	struct sched_group *most_spare_sg = NULL;
	unsigned long min_runnable_load = ULONG_MAX;
	unsigned long this_runnable_load = ULONG_MAX;
	unsigned long min_avg_load = ULONG_MAX, this_avg_load = ULONG_MAX;
	unsigned long most_spare = 0, this_spare = 0;
	int load_idx = sd->forkexec_idx;
	int imbalance_scale = 100 + (sd->imbalance_pct-100)/2;

	// 不平衡量定义为nice0的负载,即1024
	unsigned long imbalance = scale_load_down(NICE_0_LOAD) *
				(sd->imbalance_pct-100) / 100;

	if (sd_flag & SD_BALANCE_WAKE)
		load_idx = sd->wake_idx;

	do {
		unsigned long load, avg_load, runnable_load;
		unsigned long spare_cap, max_spare_cap;
		int local_group;
		int i;

		// 如果当前组和进程允许的cpu没有交集,则遍历下个组
		if (!cpumask_intersects(sched_group_span(group),
					&p->cpus_allowed))
			continue;

		// 如果这个cpu属于这个组,则记录本地组
		local_group = cpumask_test_cpu(this_cpu,
					       sched_group_span(group));

		// 统计调度组内的运行时负载和平均负载
		avg_load = 0;
		runnable_load = 0;
		max_spare_cap = 0;

		for_each_cpu(i, sched_group_span(group)) {
			/* Bias balancing toward CPUs of our domain */
			if (local_group)
				load = source_load(i, load_idx);
			else
				load = target_load(i, load_idx);

			runnable_load += load;

			avg_load += cfs_rq_load_avg(&cpu_rq(i)->cfs);

			spare_cap = capacity_spare_wake(i, p);

			if (spare_cap > max_spare_cap)
				max_spare_cap = spare_cap;
		}

		// 根据组算力调整负载
		avg_load = (avg_load * SCHED_CAPACITY_SCALE) /
					group->sgc->capacity;
		runnable_load = (runnable_load * SCHED_CAPACITY_SCALE) /
					group->sgc->capacity;

		if (local_group) {
			// 记录本地组的数据,方便在后面做判断
			this_runnable_load = runnable_load;
			this_avg_load = avg_load;
			this_spare = max_spare_cap;
		} else {
			if (min_runnable_load > (runnable_load + imbalance)) {
				// 如果当前组再加上一个nice0进程的负载量还小于最小的运行负载,
				// 则当前组是最小的运行负载
				min_runnable_load = runnable_load;
				min_avg_load = avg_load;
				idlest = group;
			} else if ((runnable_load < (min_runnable_load + imbalance)) &&
				   (100*min_avg_load > imbalance_scale*avg_load)) {
				// 前后2个组的负载非常接近,只更新一下平均负载
				min_avg_load = avg_load;
				idlest = group;
			}

			if (most_spare < max_spare_cap) {
				most_spare = max_spare_cap;
				most_spare_sg = group;
			}
		}
	} while (group = group->next, group != sd->groups);

	/*
	 * The cross-over point between using spare capacity or least load
	 * is too conservative for high utilization tasks on partially
	 * utilized systems if we require spare_capacity > task_util(p),
	 * so we allow for some task stuffing by using
	 * spare_capacity > task_util(p)/2.
	 *
	 * Spare capacity can't be used for fork because the utilization has
	 * not been set yet, we must first select a rq to compute the initial
	 * utilization.
	 */
	if (sd_flag & SD_BALANCE_FORK)
		goto skip_spare;

	if (this_spare > task_util(p) / 2 &&
	    imbalance_scale*this_spare > 100*most_spare)
		return NULL;

	if (most_spare > task_util(p) / 2)
		return most_spare_sg;

skip_spare:
	if (!idlest)
		return NULL;

	/*
	 * When comparing groups across NUMA domains, it's possible for the
	 * local domain to be very lightly loaded relative to the remote
	 * domains but "imbalance" skews the comparison making remote CPUs
	 * look much more favourable. When considering cross-domain, add
	 * imbalance to the runnable load on the remote node and consider
	 * staying local.
	 */
	if ((sd->flags & SD_NUMA) &&
	    min_runnable_load + imbalance >= this_runnable_load)
		return NULL;

	if (min_runnable_load > (this_runnable_load + imbalance))
		return NULL;

	if ((this_runnable_load < (min_runnable_load + imbalance)) &&
	     (100*this_avg_load < imbalance_scale*min_avg_load))
		return NULL;

	return idlest;
}

/**
选择兄弟空闲cpu
p: 要运行的进程
prev: 之前运行的cpu
target: 目标运行的cpu
*/
static int select_idle_sibling(struct task_struct *p, int prev, int target)
{
	struct sched_domain *sd;
	int i, recent_used_cpu;

	// 如果目标cpu是空闲的,则直接返回
	if (available_idle_cpu(target))
		return target;

	/*
	 * If the previous CPU is cache affine and idle, don't be stupid:
	 */
	// 如果目标cpu不空闲,但是prev空闲,且prev和target之间共享缓存,
	// 则返回prev cpu
	if (prev != target && cpus_share_cache(prev, target) && available_idle_cpu(prev))
		return prev;

	/* Check a recently used CPU as a potential idle candidate: */

	// 如果prev和target都不符合条件,则判断最近使用的cpu是否空闲,
	// 并且符合其它条件,符合的话返回最近使用的cpu
	recent_used_cpu = p->recent_used_cpu;
	if (recent_used_cpu != prev &&
	    recent_used_cpu != target &&
	    cpus_share_cache(recent_used_cpu, target) &&
	    available_idle_cpu(recent_used_cpu) &&
	    cpumask_test_cpu(p->recent_used_cpu, &p->cpus_allowed)) {
		/*
		 * Replace recent_used_cpu with prev as it is a potential
		 * candidate for the next wake:
		 */
		p->recent_used_cpu = prev;
		return recent_used_cpu;
	}

	// 走到这里说明上面三个cpu都不符合条件

	// sd_llc是target cpu所有的调度域的最高级缓存共享调度域
	sd = rcu_dereference(per_cpu(sd_llc, target));
	if (!sd)
		return target;

	// 选择一个空闲的核,如果没有打开SMT配置选项,这个函数直接返回-1
	i = select_idle_core(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	// 如果没有空闲的核,选择一个空闲的cpu
	i = select_idle_cpu(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	// 如果没有空闲的cpu,则选择一个target上的空闲的超线程cpu(逻辑cpu)
	// select_idle_smt只在CONFIG_SMT打开时才有效,否则返回-1
	i = select_idle_smt(p, sd, target);
	if ((unsigned)i < nr_cpumask_bits)
		return i;

	// 走到这里,意味着全都不符合,那就直接返回target
	return target;
}

/*
原文注释:
	扫描整个 LLC 域以查找空闲内核; 如果系统中没有空闲内核,则动态关闭; 
	通过 sd_llc->shared->has_idle_cores 跟踪并通过上面的 update_idle_core() 启用。
 */
static int select_idle_core(struct task_struct *p, struct sched_domain *sd, int target)
{
	// cpu集合
	struct cpumask *cpus = this_cpu_cpumask_var_ptr(select_idle_mask);
	int core, cpu;

	// 如果sched_smt_present为空,则返回-1
	if (!static_branch_likely(&sched_smt_present))
		return -1;

	// 如果当前cpu所在的共享域没有空闲核,则返回-1
	if (!test_idle_cores(target, false))
		return -1;

	// cpus 等于 调度域范围内的cpu和进程允许cpu的并集
	// todo: 函数的第一行对cpus的赋值有什么意义
	cpumask_and(cpus, sched_domain_span(sd), &p->cpus_allowed);

	// 从target开始遍历cpus里的所有cpu
	for_each_cpu_wrap(core, cpus, target) {
		bool idle = true;

		// 遍历核内的所有超线程cpu
		for_each_cpu(cpu, cpu_smt_mask(core)) {

			// 从cpus集合里移除这个cpu
			cpumask_clear_cpu(cpu, cpus);

			// 如果当前cpu不空闲,则设置不空闲标志
			if (!available_idle_cpu(cpu))
				idle = false;
		}

		// 如果这个核内有空闲cpu,则返回这个核
		if (idle)
			return core;
	}

	// 走到这里表示在target所有的共享调度域内没找到空闲的核,
	// 设置共享调度域的has_idle_cores为0,这个值就是前面test_idle_cores判断的值
	set_idle_cores(target, 0);

	return -1;
}

/*
原文注释:
	扫描LLC域中的空闲CPU;通过比较平均扫描成本(在sd->avg_scan_cost中跟踪)
	与此rq的平均空闲时间(如rq->avg_idle中所示),可以动态调整这一点。
 */
static int select_idle_cpu(struct task_struct *p, struct sched_domain *sd, int target)
{
	struct sched_domain *this_sd;
	u64 avg_cost, avg_idle;
	u64 time, cost;
	s64 delta;
	int cpu, nr = INT_MAX;

	// 获取当前cpu的共享缓存调度域
	this_sd = rcu_dereference(*this_cpu_ptr(&sd_llc));
	if (!this_sd)
		return -1;

	/*
	 原文注释:
	 	由于方差较大,我们需要一个较大的模糊因子;在这里,黑客特别敏感。
	 todo: 不知所云
	 */
	// 将平均空闲时间缩小512倍
	// todo: why?
	avg_idle = this_rq()->avg_idle / 512;

	// 平均扫描花费+1
	avg_cost = this_sd->avg_scan_cost + 1;

	// SIS_AVG_CPU默认为false
	if (sched_feat(SIS_AVG_CPU) && avg_idle < avg_cost)
		return -1;

	// SIS_PROP默认为true
	if (sched_feat(SIS_PROP)) {
		// span_weight好像是域里的总权重
		// todo: span_avg是啥意思
		u64 span_avg = sd->span_weight * avg_idle;

		// 下面计算出来的nr至少为4
		if (span_avg > 4*avg_cost)
			nr = div_u64(span_avg, avg_cost);
		else
			nr = 4;
	}

	time = local_clock();

	// 遍历调度域里的cpu
	for_each_cpu_wrap(cpu, sched_domain_span(sd), target) {
		// nr为遍历的次数,上面计算出来的至少是4
		// todo: 为什么要限制nr次数
		if (!--nr)
			return -1;
		// 进程p不允许在此cpu上运行
		if (!cpumask_test_cpu(cpu, &p->cpus_allowed))
			continue;
		// 如果这个cpu空闲,则返回
		if (available_idle_cpu(cpu))
			break;
	}

	// 计算遍历域里cpu的花费
	time = local_clock() - time;
	cost = this_sd->avg_scan_cost;
	delta = (s64)(time - cost) / 8;
	this_sd->avg_scan_cost += delta;

	return cpu;
}

static int select_idle_smt(struct task_struct *p, struct sched_domain *sd, int target)
{
	int cpu;

	// todo: sched_smt_present不知道是啥意思
	// 既然是likely那这个条件应该不常走
	if (!static_branch_likely(&sched_smt_present))
		return -1;

	// 遍历target里的cpu
	// 如果target是个核,应该会有多个cpu,否则就只有一个cpu
	for_each_cpu(cpu, cpu_smt_mask(target)) {
		// 如果进程不允许在这个cpu上运行,则继续
		if (!cpumask_test_cpu(cpu, &p->cpus_allowed))
			continue;
		// 如果cpu空闲,则返回这个cpu
		if (available_idle_cpu(cpu))
			return cpu;
	}

	return -1;
}

/**
回滚进程的运行队列

因为在负载均衡时没有选择到合适的cpu,所以这里还要回到之前的运行队列
cpu: 进程之前运行的cpu
p: 要唤醒的进程
*/
static int select_fallback_rq(int cpu, struct task_struct *p)
{
	// numa节点id
	int nid = cpu_to_node(cpu);
	const struct cpumask *nodemask = NULL;
	enum { cpuset, possible, fail } state = cpuset;
	int dest_cpu;

	/*
	原文注释:

	 如果 CPU 所在的节点已离线,则 cpu_to_node() 将返回 -1。 
	 节点上没有CPU,我们应该选择另一个节点上的CPU。
	 */

	if (nid != -1) {
		// 这个分支表示cpu所在的numa节点在线

		// numa域内的所有cpu节点
		nodemask = cpumask_of_node(nid);

		// 在numa节点内找一个激活的并且进程p可以在上面运行的cpu,
		// 然后 返回该cpu
		for_each_cpu(dest_cpu, nodemask) {
			if (!cpu_active(dest_cpu))
				continue;
			if (cpumask_test_cpu(dest_cpu, &p->cpus_allowed))
				return dest_cpu;
		}
	}

	// 走到这里就是所在的numa节点已离线。
	// 这个分支应该不经常走吧
	for (;;) {
		/* Any allowed, online CPU? */
		for_each_cpu(dest_cpu, &p->cpus_allowed) {
			// 判断目标cpu能不能运行进程p,可以运行的话就直接返回
			// 目标cpu
			if (!is_cpu_allowed(p, dest_cpu))
				continue;

			goto out;
		}

		/* No more Mr. Nice Guy. */
		/* 没有好人了 */
		// 走到这里就表示一个能用的cpu也没有
		switch (state) {
		case cpuset:
			if (IS_ENABLED(CONFIG_CPUSETS)) {
				// 如果配置了CONFIG_CPUSETS,则强制设置
				// 进程允许运行的cpu,因为进程没有一个能用的cpu
				cpuset_cpus_allowed_fallback(p);
				state = possible;
				break;
			}
			/* Fall-through */
		case possible:
			// 如果没有开启CONFIG_CPUSETS,则将进程的cpumask
			// 设置为cpu_possible_mask
			do_set_cpus_allowed(p, cpu_possible_mask);
			state = fail;
			break;

		case fail:
			BUG();
			break;
		}
	}

out:
	if (state != cpuset) {
		/*
		原文注释:
			不要告诉他们关于移动退出任务或内核线程
			(都是 mm NULL),因为它们永远不会离开内核。

			打印一条日志
		 */
		if (p->mm && printk_ratelimit()) {
			printk_deferred("process %d (%s) no longer affine to cpu%d\n",
					task_pid_nr(p), p->comm, cpu);
		}
	}

	return dest_cpu;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值