Linux | Misfit task migration

在分析 Android 性能问题,尤其是涉及 摆核场景时,我们经常会遇到一种现象:

一个处于 running 状态 的任务会因为被标记为 "misfit" 而被系统主动迁移到大核上。

这种是最常见的情形,也有misfit task 发起和大核task swap 这种特殊情形(MTK 平台,下面会细说)

对于misfit,我们自然会产生这样的疑问:

  1. 什么样的任务会被标记为 misfit?
  2. misfit 任务的迁移目标 CPU 是如何选择的?

先罗列出来一些关键函数

更新"misfit" 状态

kernel/sched/fair.c

c 复制代码
inline void update_misfit_status(struct task_struct *p, struct rq *rq)
{
	int cpu = cpu_of(rq);
	bool need_update = true;

	trace_android_rvh_update_misfit_status(p, rq, &need_update);
	if (!sched_asym_cpucap_active() || !need_update)
		return;

	/*
	 * Affinity allows us to go somewhere higher?  Or are we on biggest
	 * available CPU already? Or do we fit into this CPU ?
	 */
	if (!p || (p->nr_cpus_allowed == 1) ||
	    (arch_scale_cpu_capacity(cpu) == p->max_allowed_capacity) ||
	    task_fits_cpu(p, cpu)) {

		rq->misfit_task_load = 0;
		return;
	}

	/*
	 * Make sure that misfit_task_load will not be null even if
	 * task_h_load() returns 0.
	 */
	rq->misfit_task_load = max_t(unsigned long, task_h_load(p), 1);
}
EXPORT_SYMBOL_GPL(update_misfit_status);

转换为白话解释:

js 复制代码
update_misfit_status(p, rq)
│
├── 判断是否需要更新:
│ └── 如果调度器未启用异构 CPU 支持(sched_asym_cpucap_active() 为 false)
│ └── 或 need_update == false(给厂商用的钩子),直接返回。
│
├── 检查任务是否"fit"当前 CPU,满足以下任一条件则视为"fit":
│ ├── 任务为空(NULL)
│ ├── 任务只能运行在一个 CPU 上(p->nr_cpus_allowed == 1)
│ ├── 当前 CPU 已是该任务能跑的最大性能核(arch_scale_cpu_capacity(cpu) == p->max_allowed_capacity)
│ └── 任务适合当前 CPU(task_fits_cpu(p, cpu) 为 true)
│ └── 若满足上述任一条件,清除 misfit 状态:rq->misfit_task_load = 0 并返回。
│
└── 否则说明当前任务"misfit" 当前 CPU(即"跑错核" 或者说"跑不动"),设置 misfit 状态:
  └── rq->misfit_task_load = max(task_h_load(p), 1),即即使负载为 0,也至少标记为 1。

注:

rq->misfit_task_load 这一用于判断当前cpu 是否存在misfit task 的函数,在很多场景判断下都会用到。

task_h_load(p) 函数用于计算任务 p 在其 CFS 运行队列中的加权负载

c 复制代码
static unsigned long task_h_load(struct task_struct *p)
{
	struct cfs_rq *cfs_rq = task_cfs_rq(p);

	update_cfs_rq_h_load(cfs_rq);
	return div64_ul(p->se.avg.load_avg * cfs_rq->h_load,
			cfs_rq_load_avg(cfs_rq) + 1);
}

更新"misfit" 状态的场景

可以搜"update_misfit_status" 用到的地方,一个是tick,这点很容易理解,还有一个就是非常熟悉的pick_next_task_fair 函数了。

MTK 针对misfit 的客制化

tick → hook_scheduler_tick → check_for_migration

check_for_migration

c 复制代码
void check_for_migration(struct task_struct *p)
{
	int new_cpu = -1, better_idle_cpu = -1;
	int cpu = task_cpu(p);
	struct rq *rq = cpu_rq(cpu);

	irq_log_store();

	if (rq->misfit_task_load) {
		struct em_perf_domain *pd;
		struct cpufreq_policy *policy;
		int opp_curr = 0, thre = 0, thre_idx = 0;

		if (rq->curr->__state != TASK_RUNNING ||
			rq->curr->nr_cpus_allowed == 1)
			return;

		pd = em_cpu_get(cpu);
		if (!pd)
			return;

		thre_idx = (pd->nr_perf_states >> 3) - 1;
		if (thre_idx >= 0)
			thre = pd->table[thre_idx].frequency;

		policy = cpufreq_cpu_get(cpu);
		irq_log_store();

		if (policy) {
			opp_curr = policy->cur;
			cpufreq_cpu_put(policy);
		}

		if (opp_curr <= thre) {
			irq_log_store();
			return;
		}

		raw_spin_lock(&migration_lock);
		irq_log_store();
		raw_spin_lock(&p->pi_lock);
		irq_log_store();

		new_cpu = p->sched_class->select_task_rq(p, cpu, WF_TTWU);
		irq_log_store();

		raw_spin_unlock(&p->pi_lock);

		if ((new_cpu < 0) || new_cpu >= MAX_NR_CPUS ||
			(cpu_cap_ceiling(new_cpu) <= cpu_cap_ceiling(cpu)))
			better_idle_cpu = select_bigger_idle_cpu(p);

		if (better_idle_cpu >= 0)
			new_cpu = better_idle_cpu;

		if (new_cpu < 0) {
			raw_spin_unlock(&migration_lock);
			irq_log_store();
			return;
		}

		irq_log_store();
		if ((better_idle_cpu >= 0) ||
			(new_cpu < MAX_NR_CPUS && new_cpu >= 0 &&
			(cpu_cap_ceiling(new_cpu) > cpu_cap_ceiling(cpu)))) {
			raw_spin_unlock(&migration_lock);

			migrate_running_task(new_cpu, p, rq, MIGR_TICK_PULL_MISFIT_RUNNING);
			irq_log_store();
		} else {
#if IS_ENABLED(CONFIG_MTK_SCHED_BIG_TASK_ROTATE)
			int thre_rot = 0, thre_rot_idx = 0;

			thre_rot_idx = (pd->nr_perf_states >> 1) - 1;
			if (thre_rot_idx >= 0)
				thre_rot = pd->table[thre_rot_idx].frequency;

			if (opp_curr > thre_rot) {
				task_check_for_rotation(rq);
				irq_log_store();
			}

#endif
			raw_spin_unlock(&migration_lock);
		}
	}
	irq_log_store();
}

转换为白话解释:

js 复制代码
check_for_migration(p)
│
│ 只有rq->misfit_task_load 为真,才会往下走,说明当前队列存在misfit 的情况
│ │
│ ├── 当前任务没有running,或者有限制到只能运行在一个 CPU 上,就不用迁移
│ │
│ ├── 获取当前 CPU 所属的能耗性能域(pd)
│ │ └── 如果拿不到 pd,说明调度域异常,也直接 return
│ │
│ ├── 计算"低频阈值"(thre),用于判断当前 CPU 频率是否太低
│ │ └── thresh_idx = pd->nr_perf_states >> 3(等于除8)
│ │
│ ├── 获取当前 CPU 实际运行频率(opp_curr),和 thre 比一下
│ │ └── 如果当前频率 opp_curr <= thre,说明是因为频率低所以不fit,任务迁移意义不大 → return
│ │
│ ├── 获取 p 的目标 CPU:调用 select_task_rq() 让调度类决定迁到哪个 CPU(new_cpu)
│ │
│ ├── 如果 select_task_rq 给的目标 CPU 不行(new_cpu < 0,或new cpu 的ceiling没提升)
│ │ └── 那么久尝试找一个性能更强的空闲 CPU(select_bigger_idle_cpu)
│ │
│ ├── 如果到这里还找不到合适的 CPU,一般很难发生,说明选核也没选到,选更大的idle cpu 也没有,直接return
│ │
│ ├── 如果new CPU 比当前的强,或者找到了 idle CPU:
│ │ └── 执行 migrate_running_task() 做实际迁移(此时任务是 running 状态)
│ │
│ └── 否则,进入"旋转尝试"逻辑(下面会细说task_check_for_rotation):
│     └── 如果当前 CPU 频率 > 另一个更高的旋转阈值 thre_rot
│         └── 尝试 task_check_for_rotation()

task_check_for_rotation

c 复制代码
void task_check_for_rotation(struct rq *src_rq)
{
	u64 wc, wait, max_wait = 0, run, max_run = 0;
	int deserved_cpu = nr_cpu_ids, dst_cpu = nr_cpu_ids;
	int i, src_cpu = cpu_of(src_rq);
	struct rq *dst_rq;
	struct task_rotate_work *wr = NULL;
	struct root_domain *rd = cpu_rq(smp_processor_id())->rd;
	int force = 0;
	struct cpumask src_eff;
	struct cpumask dst_eff;
	bool src_ls;
	bool dst_ls;

	irq_log_store();

	if (!big_task_rotation_enable) {
		irq_log_store();
		return;
	}

	if (is_max_capacity_cpu(src_cpu)) {
		irq_log_store();
		return;
	}

	if (cpu_paused(src_cpu)) {
		irq_log_store();
		return;
	}

	if (!(rd->android_vendor_data1[0])) {
		irq_log_store();
		return;
	}

	irq_log_store();
	wc = ktime_get_raw_ns();
	irq_log_store();
	for_each_possible_cpu(i) {
		struct rq *rq = cpu_rq(i);
		struct rot_task_struct *rts;

		if (cpu_paused(i))
			continue;

		if (!is_min_capacity_cpu(i))
			continue;

		if (!READ_ONCE(rq->misfit_task_load) ||
			(READ_ONCE(rq->curr->policy) != SCHED_NORMAL))
			continue;

		if (is_reserved(i))
			continue;

		compute_effective_softmask(rq->curr, &dst_ls, &dst_eff);
		if (dst_ls && !cpumask_test_cpu(src_cpu, &dst_eff))
			continue;

		rts = &((struct mtk_task *) rq->curr->android_vendor_data1)->rot_task;
		wait = wc - READ_ONCE(rts->ktime_ns);

		if (wait > max_wait) {
			max_wait = wait;
			deserved_cpu = i;
		}
	}
	irq_log_store();

	if (deserved_cpu != src_cpu) {
		irq_log_store();
		return;
	}

	for_each_possible_cpu(i) {
		struct rq *rq = cpu_rq(i);
		struct rot_task_struct *rts;

		if (cpu_paused(i))
			continue;

		if (capacity_orig_of(i) <= capacity_orig_of(src_cpu))
			continue;

		if (READ_ONCE(rq->curr->policy) != SCHED_NORMAL)
			continue;

		if (READ_ONCE(rq->nr_running) > 1)
			continue;

		if (is_reserved(i))
			continue;

		compute_effective_softmask(rq->curr, &dst_ls, &dst_eff);
		if (dst_ls && !cpumask_test_cpu(src_cpu, &dst_eff))
			continue;

		rts = &((struct mtk_task *) rq->curr->android_vendor_data1)->rot_task;
		run = wc - READ_ONCE(rts->ktime_ns);

		if (run < TASK_ROTATION_THRESHOLD_NS)
			continue;

		if (run > max_run) {
			max_run = run;
			dst_cpu = i;
		}
	}
	irq_log_store();

	if (dst_cpu == nr_cpu_ids) {
		irq_log_store();
		return;
	}

	dst_rq = cpu_rq(dst_cpu);
	irq_log_store();
	double_rq_lock(src_rq, dst_rq);
	irq_log_store();

	if (dst_rq->curr->policy == SCHED_NORMAL) {
		if (!cpumask_test_cpu(dst_cpu,
					src_rq->curr->cpus_ptr) ||
			!cpumask_test_cpu(src_cpu,
					dst_rq->curr->cpus_ptr)) {
			double_rq_unlock(src_rq, dst_rq);
			irq_log_store();
			return;
		}

		if (cpu_paused(src_cpu) || cpu_paused(dst_cpu)) {
			double_rq_unlock(src_rq, dst_rq);
			irq_log_store();
			return;
		}

		compute_effective_softmask(dst_rq->curr, &dst_ls, &dst_eff);
		if (dst_ls && !cpumask_test_cpu(src_cpu, &dst_eff)) {
			double_rq_unlock(src_rq, dst_rq);
			return;
		}

		compute_effective_softmask(src_rq->curr, &src_ls, &src_eff);
		if (src_ls && !cpumask_test_cpu(dst_cpu, &src_eff)) {
			double_rq_unlock(src_rq, dst_rq);
			return;
		}

		if (!src_rq->active_balance && !dst_rq->active_balance) {
			src_rq->active_balance = 1;
			dst_rq->active_balance = 1;

			irq_log_store();
			get_task_struct(src_rq->curr);
			irq_log_store();
			get_task_struct(dst_rq->curr);
			irq_log_store();

			wr = &per_cpu(task_rotate_works, src_cpu);

			wr->src_task = src_rq->curr;
			wr->dst_task = dst_rq->curr;

			wr->src_cpu = src_rq->cpu;
			wr->dst_cpu = dst_rq->cpu;
			force = 1;
		}
	}
	irq_log_store();
	double_rq_unlock(src_rq, dst_rq);
	irq_log_store();

	if (force) {
		queue_work_on(src_cpu, system_highpri_wq, &wr->w);
		irq_log_store();
		trace_sched_big_task_rotation(wr->src_cpu, wr->dst_cpu,
					wr->src_task->pid, wr->dst_task->pid,
					false);
	}
	irq_log_store();
}

白话解释:

js 复制代码
task_check_for_rotation(src_rq)
│
│ ├── big_task_rotation_enable 这个mtk feature 如果没开就 return
│ ├── 当前 CPU 已经是最大性能核就 return
│ ├── 当前 CPU 被 pause 了 → return
│ └── "互换任务之前,先看看两边的任务能不能跑到对方 CPU 上(根据cpumask判断),一方不允许,就return。
│
├── 开始进入正文,Step1:先遍历所有 CPU,尝试找一个"饿的最久的小核任务"
│ ├── 筛选条件:是小核 + misfit + SCHED_NORMAL + 不是保留核
│ ├── 判断它等了多久(wait = 当前时间 - 它的 ktime_ns),这里是mtk 客制化得出的数据
│ └── 挑出"等待时间最长"的小核任务所在的 CPU → 作为deserved_cpu
│
├── 上面一轮的小核遍历下来,如果得出的deserved_cpu 并非是src_cpu
├── 说明小核上还有"饿的"比自己更久的,就说明机会应该先轮到别人,return
│
├── Step2:再次遍历所有 CPU,尝试找一个"可以和p 互换的大核task"
│ ├── 筛选条件:更大的cluster 上的cpu(就是大核) + 当前任务是 SCHED_NORMAL + 只运行一个任务
│ ├── 判断这个任务已经运行了多久(run = 当前时间 - ktime_ns)
│ └── 找出 run 时间最长的目标 CPU → dst_cpu
│
├── 上面的遍历如果没找到合适的大核目标 CPU → return
│
├── 锁住 src_rq 和 dst_rq,准备开始换任务
│ ├── 确保两个任务的 cpu 亲和性没问题(互相都能跑到对方的 CPU)
│ ├── 检查是否被 pause 了
│ ├── 如果两个 CPU 都没有 active_balance,就可以开始换
│ │ ├── 标记 active_balance = 1,这个是防止并发
│ │ ├── 增加任务引用计数(get_task_struct)
│ │ └── 设置好 task_rotate_work 的数据结构
│
├── 解锁 runqueue,最后:
│ └── 如果符合条件,就把任务 rotate 互换(queue 到 workqueue 上处理)
│ └── 一般Systrace 中也可以直接看出来,就是后面先跟一个kwork然后紧跟着一个migration 线程

真正执行swap 的函数

c 复制代码
static void task_rotate_work_func(struct work_struct *work)
{
	struct task_rotate_work *wr = container_of(work,
				struct task_rotate_work, w);

	int ret = -1;
	struct rq *src_rq, *dst_rq;

	irq_log_store();
	ret = migrate_swap(wr->src_task, wr->dst_task,
			task_cpu(wr->dst_task), task_cpu(wr->src_task));
	irq_log_store();

	if == 0) {
		trace_sched_big_task_rotation(wr->src_cpu, wr->dst_cpu,
						wr->src_task->pid,
						wr->dst_task->pid,
						true);
	}

	irq_log_store();
	put_task_struct(wr->src_task);
	irq_log_store();
	put_task_struct(wr->dst_task);
	irq_log_store();

	src_rq = cpu_rq(wr->src_cpu);
	dst_rq = cpu_rq(wr->dst_cpu);

	irq_log_store();
	local_irq_disable();
	double_rq_lock(src_rq, dst_rq);
	irq_log_store();
	src_rq->active_balance = 0;
	dst_rq->active_balance = 0;
	irq_log_store();
	double_rq_unlock(src_rq, dst_rq);
	local_irq_enable();
	irq_log_store();
}

解释:

js 复制代码
task_rotate_work_func(work)
│
├── 通过 container_of() 拿到 task_rotate_work 指针
│   └── 里面包含了两个准备swap 的task:src_task 和 dst_task
│
├── 调用 migrate_swap()
│   └── 尝试把 src_task 和 dst_task 两个任务的运行 CPU 互换
│
├── migrate_swap 成功(返回 0):

这里涉及到一个关键函数migrate_swap,忽略部分调用过程,最后会调用到关键函数 move_queued_task_locked ,这个函数作用是将任务从一个 CPU 的 runqueue 迁移到另一个 CPU 的 runqueue

js 复制代码
static inline
void move_queued_task_locked(struct rq *src_rq, struct rq *dst_rq, struct task_struct *task)
{
	lockdep_assert_rq_held(src_rq);
	lockdep_assert_rq_held(dst_rq);

	deactivate_task(src_rq, task, 0);
	set_task_cpu(task, dst_rq->cpu);
	activate_task(dst_rq, task, 0);
}
相关推荐
涛ing33 分钟前
【Linux “less“ 命令详解】
linux·运维·c语言·c++·人工智能·vscode·bash
_一条咸鱼_1 小时前
Android ARouter 处理器模块深度剖析(三)
android·面试·android jetpack
_一条咸鱼_2 小时前
Android ARouter 基础库模块深度剖析(四)
android·面试·android jetpack
_一条咸鱼_2 小时前
Android ARouter 核心路由模块原理深度剖析(一)
android·面试·android jetpack
火柴就是我2 小时前
android 基于 PhotoEditor 这个库 开发类似于dlabel的功能
android
_一条咸鱼_2 小时前
Android ARouter 编译器模块深度剖析(二)
android·面试·android jetpack
林木木木木木木木木木3 小时前
【随身WiFi】随身WiFi Debian系统优化教程
linux·运维·debian·随身wifi
Gracker3 小时前
Android Weekly #202515
android
临观_3 小时前
打靶日记 zico2: 1
linux·网络安全
痆古酊旳琲伤3 小时前
Linux驱动开发1 - Platform设备
linux·驱动开发