前言
本文基于MTK 平台、kernel 6.1
CPU 调度这块的资料网上一抓一大把,其中很多分析源码细节不错的文章,多是分析CFS 的。 所以本文只是挑了几个场景,画了几个框图以及选了两个案例分析。
唤醒场景下的选核
选核涉及多种不同的场景,这里以fair task 唤醒选核为例进行说明。
无论任务是新创建(通过 fork
)还是从阻塞状态唤醒,调度器都会通过 select_task_rq_fair
接口进行选核。
MTK 平台上hook 了#1 及#2,分别用于task_tubro 以及EAS 场景下的选核
注:task_turbo 选核只作用于应用启动环节,所以启动过程中task 只走task_turbo 选核。
Task turbo 选核
图中可知,task_turbo 的选核是从大核开始选,先优先选idle,然后选剩余计算能力最大的核。
EAS 选核
这里只画了主干路径,一些不常见的路径或feature 强相关的没有体现
注:EAS 只有在未处于overutilized 状态下才会启用,至于overutilized 如何判定,mtk 有客制化。
下面分Part1 & Part2 介绍。
Part1:Root domain
overload 和overutilized 这两个是不同的概念
- CPU overload:runqueue 有 ≥2 任务,或 1 个 misfit task。
- CPU overutilized:utility > capacity(预留 20% 算力)。
- Root Domain overload:至少一个 CPU overload。
- Root Domain overutilized:至少一个 CPU overutilized。
注:
这里指的是原生上的判断方法,对于overutilized 判断可以看出原生上更容易触及overutilized 的条件
Part2:判定overutilized
Mtk hook 后,遍历该CPU 所属cluster 中的所有cpu,统计出该cluster 总的util ,然后判断如果总的util 超过该cluster 总容量的 80%,则认定当前cluster 处于 overutilized 状态。可以看出这里有别于原生,相对来说更不容易触及overutilized 的条件。
注:对于手机平台而言,root domain就一个,涵盖所有CPU
Ftrace eg:
Part3:Sync 唤醒且Curr cpu running =1
这个条件比较苛刻,简单了解下即可。
要求:wake_flags 包含WF_SYNC
,且这个进程没有正在退出,及需fit 这个cpu,且cur cpu 队列上只有一个任务。
#define WF_SYNC 0x10 /* Waker goes to sleep after wakeup */
select reason: #define LB_SYNC (0x02)
Part4:选择候选CPU 集合
这里是重点,这块策略mtk 也客制化了,同样的我们只梳理主干路径,跳过一些比如判断是否处于中断、CPU 是否被限流、是否是latency_sensitive 等路径。
1.先遍历cluster,并遍历cluster 中每个CPU
2.记录CPU 的利用率(cpu_util)和空闲容量(spare_cap)。
3.有些情况下这些cpu 是直接跳过的,不会作为候选CPU,比如像mask 不满足(通常是上层设置了cpuset 限制或Cgroup 组限制,只能运行在指定的cpu 集上)、处于高irq中、还有像这个task util 比较大不适合运行在该CPU 上等(可能会经过sugov放大)。
4.如果task 是VIP ,则优先选择 VIP 任务数量最少的 CPU。如果当前CPU 的 VIP 任务数量比之前的最小值更小,则更新候选 CPU 集合。
这里其实是会进一步缩小候选cpu 的范围,一定程度也会影响到功耗,这个比较好理解,因为最节能的CPU 不一定是vip 数最少的核。
/*don't choice CPU only because it can calc energy, choice min_num_vip CPU */
if ((prev_min_num_vip != UINT_MAX) && (prev_min_num_vip != min_num_vip))
cpumask_clear(candidates);
针对这段代码,举个例子比如此时候选CPU 是CPU 5以及CPU 6,它们上面的vip 数目分别是2和3,此时遍历到CPU 7时,它的上面vip 数目只有1,此时清空CPU 5以及CPU 6,将CPU 7加入候选。
注:
挑选剩余计算能力最大的CPU 并不意味着功耗高,相反很多时候会让CPU 跑到更低的freq 上,反而节省功耗。这点取决于该cpu 所处的opp 档位,小cluster 的高opp 档位的功耗可能会比大核cluster 的低opp 档位的功耗要高
4. 比较剩余最大计算能力的CPU
如果p 是VIP,则依旧保持最少vip 数目的CPU优先,如果数目和min_vip_num 一致的话,优先选大的。如果p 是非VIP 的话,则优先选择剩余计算能力大的的CPU。
Part5:候选CPU 集 中选最节能的CPU
从候选 CPU 集合(candidates)中选择一个<能量最优>的 CPU 来运行任务 p。 通过计算每个候选 CPU 的能量消耗差异(cur_delta),选择能量消耗最小的 CPU 作为最佳选择。
EAS覆盖了CFS的任务唤醒平衡代码。在唤醒平衡时,它使用平台的EM和PELT信号来选择节能的目标CPU。当EAS被启用时,select_task_rq_fair()调用find_energy_efficient_cpu() 来做任务放置决定。这个函数寻找在每个性能域中寻找具有最高剩余算力(CPU算力 - CPU利用率)的CPU,因为它能让我们保持最低的频率。然后,该函数检查将任务放在新CPU相较依然放在之前活动的prev_cpu是否可以节省能量.
如果唤醒的任务被迁移,find_energy_efficient_cpu()使用compute_energy()来估算 系统将消耗多少能量。compute_energy()检查各CPU当前的利用率情况,并尝试调整来 “模拟”任务迁移。EM框架提供了API em_pd_energy()计算每个性能域在给定的利用率条件 下的预期能量消耗。
docs.kernel.org/scheduler/s…
建议可以详读这篇原文,还是比较有意思的
Trace 分析 & Energy 最优
好,现在有了前面的论述,我们通过Trace 看一个选择最优Energy 的例子
以System_Server 的InputReader 如下这个片段为例
之所以最终选择了CPU 6
可以参考上面流程图中的关键算法code,然后带入Ftrace 中的数值计算下
cur_delta = max(cur_delta, base_energy) - base_energy;
if (cur_delta < best_delta) {
best_delta = cur_delta;
best_energy_cpu = cpu;
}
CPU 3:cur_delta = max(723084, 692956) - 692956 = 723084 - 692956 = 30128
CPU 6:cur_delta = max(1814629, 1785263) - 1785263 = 1814629 - 1785263 = 29366
cur_delta 可以简单理解为表示将任务迁移到某个 CPU 后,带来的Energy 增量
EAS 这部分牵扯的内容比较多也比较复杂,后面计划另起一文详细梳理。
Load balance
这里以常见的tick balance 举例,同样的还是mtk 平台上
其它几种balance,本质都差不多,最终都会触发load_balance 这一函数
案例分析
为便于理解,下面举两个简单的例子
Migration 导致的delay
简而言之: wmshell.main 线程(本身是vip 线程)被唤醒后经过选核,选到了vip 数最少的核上,但是被紧接着的pull migration 到了一个有13个vip 线程的核上,进而造成一个9ms 的明显deley
中断关闭导致的延迟
这里可以看到cpu 3向cpu7 发了IPI,但是CPU 7 没有收到
正常片段应该类似如下
在这77ms 时间内,Sf 一直尝试选核到CPU 7上均没有成功,直到70 多ms 后才选了CPU 6.
这段时间内CPU 7上没有收到任何中断,说明中断被关闭,此时CPU 7上运行的线程正在等底层回传buffer
Task util 对选核的影响
我们都知道boost/deboost CPU frequency 常见的手段就是垫高task 的util或者削减其util,从而让它变得更大或更小,实际是通过设置sched_setattr 接口进而驱动sugov 调频的。 但实际上,改变util 也会影响选核,这点容易被忽视
这里以一个简短的例子说明下:
看到这个图,不禁会想: 看起来小核loading 很轻,这个task 为何还会跑在大核CPU4 上?如果此时跑到小核岂不是更节能?以及为何这个task 能一下子就将大核频率拉满?
//task 被唤醒
<idle>-0 (-----) [006] .... 73715.457976: sched_waking: comm=traced_probes pid=1392 prio=120 target_cpu=6
//util_ewma 采用指数加权移动平均,使得最近的负载变化对计算结果的影响更大。它更关注于最近的负载波动,并通过平滑处理避免过于剧烈的负载波动影响
<idle>-0 (-----) [006] .... 73715.457984: sched_task_util: pid=1392 util=176 util_enqueued=2147484473 util_ewma=825
//task 没有uclamp 限制
<idle>-0 (-----) [006] .... 73715.457986: sched_task_uclamp: pid=1392 util=2147484473 active=0 min=0 max=1024 min_ud=0 min_req=0 max_ud=0 max_req=1024
//非vip
<idle>-0 (-----) [006] .... 73715.458004: sched_get_vip_task_prio: comm=traced_probes pid=1392 vip_prio=-1 prio=120 is_ls=0 ls_vip_threshold=99 cpuctl=3 group_threshold=99 is_basic_vip=0
由前面框图可知,在没有过载的情况下,非VIP任务会先通过最大剩余计算能力选择候选CPU集,再从中选出最节能的CPU。 下面先从大核Cluster 开始遍历 4-7,然后再遍历小核Cluster 0-3 (是否从大核cluster 开始遍历有参数可以设置,这里不做讨论)
先计算cpu 4剩余计算能力是1006,将其临时赋值给target_max_spare_cap
下面进行CPU5 spare_cap计算
<idle>-0 (-----) [006] .... 73715.458020: sched_target_max_spare_cpu: comm=traced_probes type=sys_max_spare best_cpu=4 new_cpu=5 replace=0 is_sensitive=0 is_vip=0 num_vip=-1 min_num_vip=-1 spare_cap=1001 target_max_spare_cap=1006
cpu5 spare_cap=1001 稍稍低于CPU 4的,所以不会选择CPU5,以此类推的分析,最后发现CPU4 的spare_cap 1006是最大的。
下面将进行sched_fits_cap_ceiling 的计算,也就是看该cpu 是否满足这个task 的大小。
<idle>-0 (-----) [006] .... 73715.458022: sched_fits_cap_ceiling: fit=0 cpu=5 cpu_util=825 cup_cap=1008 ceiling=1024 capacity_dn_margin=1024 capacity_up_margin=1024 sugov_margin=1280 capacity_orig=1024 AM_enabled=1
sched_fits_cap_ceiling: fit=0 // 该cpu 不能满足这个task
cpu=5 cpu_util=825 // task 的util,实际是util_ewma
cup_cap=1008 // CPU 当前实际容量
ceiling=1024 // 最大算力
capacity_dn_margin=1024
capacity_up_margin=1024
sugov_margin=1280 // 自适应调节因子(128.0%,放大后的需求)
capacity_orig=1024 // CPU 原始算力(1024=100%,表示这是一个大核)
AM_enabled=1 // 自适应调节(adaptive margin)已启用
上面以cpu 5为例说明,fit=0 说明不能满足,因为经过放大后的需求算力超过了CPU5 能承载的最大算力
:825 * 1.280 = 1,056
,其它几个也都返回fit=0,这里不一一写出
注:大核上sugov_margin=1280,小核上sugov_margin=1024,此外小核上计算时cpu_util 就不是大核上的825 了,会被钳制到229,这块就不展开讨论了
简而言之,目前为止可知CPU4 是剩余计算能力最大的,但是0~7 都不能满足该task util。
所以在赋值候选CPU mask 以及计算最节能CPU 之前,就会跳出来循环
if (!util_fits_capacity(cpu_util, cpu_cap, cpu)) continue;
所以这个例子中,没有候选cpu 集(candidates=0),也没有计算哪个cpu 最节能
<idle>-0 (-----) [006] .... 73715.458051: sched_find_best_candidates: pid=1392 is_vip=0 candidates=0 order_index=1 end_index=0 active_mask=255 pause_mask=0 allowed_cpu_mask=127
最后选择的reason 就是LB_MAX_SPARE_CPU 即最大剩余计算能力的CPU 4
<idle>-0 (-----) [006] .... 73715.458056: sched_select_task_rq: pid=1392 compat_thread=0 in_irq=1 select_reason=512 backup_reason=0 prev_cpu=6 target_cpu=4 task_util=176 task_util_est=825 boost=825 task_mask=127 effective_softmask=255 prefer=0 sync_flag=0 cpuctl_grp_id=3 cpuset_grp_id=2
结语:
Android 上之所以采用CFS,单纯因为Android 诞生的时候Linux 已经采用了CFS,慢慢的Google 在上层顺势引入了Cgroup 分组控制等。
但是对于Android 这种强交互系统,某些场景下还不够,所以人们就自然的想到了哪些线程要优先跑,由自己来决定,于是在调度关键路径上(唤醒、抢占、负载均衡等)的一系列hook 进行客制化,也就是各家宣称的"VIP"、"MVP" 等调度策略,这些的策略的堆叠使得原本已经复杂臃肿的CFS 架构更加复杂,使用不当各种副作用(对吞吐量的影响、对功耗的影响、其本身的计算开销等)也会接踵而至。
简而言之,资源是一定的,任何策略注定都是顾此失彼,就看这个"失彼" 对用户的影响是否可以做到无感。
此外,Linux 后续版本将采用EEVDF 调度,后面将新起文章介绍
最后推荐一篇文章, 写的比较有意思,讲了CFS 存在的问题以及几个CFS 调度存在的问题案例
<The Linux Scheduler: a Decade of Wasted Cores>