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);
}
相关推荐
成工小白9 分钟前
【Linux】文件操作
linux·运维·服务器
皮皮高15 分钟前
itvbox绿豆影视tvbox手机版影视APP源码分享搭建教程
android·前端·后端·开源·tv
程序员打怪兽1 小时前
基于V4L2摄像头智能识别拍照(人脸和手掌)
linux·嵌入式
EnzoRay1 小时前
MotionEvent
android
玲小珑1 小时前
Auto.js 入门指南(七)定时任务调度
android·前端
墨狂之逸才2 小时前
adb常用命令调试
android
YoungForYou2 小时前
Android端部署NCNN
android
移动开发者1号2 小时前
Jetpack Compose瀑布流实现方案
android·kotlin
张海森-1688202 小时前
windows10搭建nfs服务器
linux
移动开发者1号2 小时前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·kotlin