经常分析Android 调度问题的话,对“Misfit migration” 这个概念想必并不陌生。 与一般场景下迁移“可运行(runnable)”任务不同,Misfit 迁移针对的是处于“运行中(running)”状态的任务。
Misfit task 迁移上古时代(2018)的修改:
sched/fair: Migrate 'misfit' tasks on asymmetric capacity systems
On asymmetric cpu capacity systems (e.g. Arm big.LITTLE) it is crucial for performance that cpu intensive tasks are aggressively migrated to high capacity cpus as soon as those become available. The capacity awareness tweaks already in the wake-up path can't handle this as such tasks might run or be runnable forever. If they happen to be placed on a low capacity cpu from the beginning they are stuck there forever while high capacity cpus may have become available in the meantime.
To address this issue this patch set introduces a new "misfit" load-balancing scenario in periodic/nohz/newly idle balance which tweaks the load-balance conditions to ignore load per capacity in certain cases. Since misfit tasks are commonly running alone on a cpu, more aggressive active load-balancing is needed too.
建议先看下当时"Misfit"这一修改以及社区的讨论过程,这样有助于理解其引入的前因后果。
最近遇到了"Misfit" 相关问题,所以打算再简单重温下这块的内容
本文将罗列一些关键函数,不用拘泥于代码细节,因为源码总是在不断的变化之中,贴出源码只是为了方便对照着解释。
理解其设计思路才是关键,理解的过程中可以不断的抛出疑问,带着疑问再不断的理解,如此往复。
更新”misfit” 状态
kernel/sched/fair.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);
转换为白话解释:
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 是否是misfit
static inline int is_misfit_task(struct task_struct *p, struct rq *rq)
{
int cpu = cpu_of(rq);
if (!p || p->nr_cpus_allowed == 1)
return 0;
if (arch_scale_cpu_capacity(cpu) == p->max_allowed_capacity)
return 0;
if (task_fits_cpu(p, cpu_of(rq)))
return 0;
return 1;
}
这里的task_fits_cpu 主要是检查给定的任务是否适合在某个特定的 CPU 上运行,它考虑了任务的 CPU 预估util (task_util_est)和 uclamp
上下限(如果有钳制的话)
以fair 为例
static inline int task_fits_cpu(struct task_struct *p, int cpu)
{
unsigned long uclamp_min = uclamp_eff_value(p, UCLAMP_MIN);
unsigned long uclamp_max = uclamp_eff_value(p, UCLAMP_MAX);
unsigned long util = task_util_est(p);
/*
* Return true only if the cpu fully fits the task requirements, which
* include the utilization but also the performance hints.
*/
return (util_fits_cpu(util, uclamp_min, uclamp_max, cpu) > 0);
}
util_fits_cpu 这一函数比较简单就不展开了,主要就是先看下util 是否fit 这个cpu(fits_capacity 判断),然后就比较uclamp 限制后是否还fit(如果有钳制的话,没钳制就直接看fits_capacity)。
task_h_load(p) 函数用于计算任务 p 在其 CFS 运行队列中的加权负载
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
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();
}
转换为白话解释:
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 状态)
│ │
│ └── 否则,进入“swap”逻辑(下面会细说task_check_for_rotation):
│ └── 如果当前 CPU 频率 > 另一个更高的旋转阈值 thre_rot
│ └── 尝试 task_check_for_rotation()
task_check_for_rotation
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();
}
白话解释:
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 的函数
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();
}
解释:
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
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);
}