前言
本文基于MTK 平台、kernel 6.10
画了几个框图以及选了两个案例分析,不会拘泥于代码细节,先有个大致的了解
唤醒场景下的选核
选核涉及多种不同的场景,这里以fair task 唤醒选核为例进行说明。 其实不论是唤醒fork task还是唤醒阻塞task,都会走到select_task_rq。
MTK 平台上hook 了#1 及#2,分别用于task_tubro 以及EAS 场景下的选核
注:task_turbo 主要是针对启动场景
Task turbo 选核
EAS 选核
这里只画了主干路径,一些不常见的路径或feature 强相关的没有体现
注:EAS 只有在未处于overutilized 状态下才会启用,至于overutilized 如何判定,mtk 有客制化。
下面分Part1 & Part2 介绍。
Part1:Root domain
对于CPU 而言,overload 和overutilized 这两个是不同的概念
- CPU overload:runqueue 有 ≥2 任务,或 1 个 misfit task。
- CPU overutilized:utility > capacity(预留 20% 算力)。
- Root Domain overload:至少一个 CPU overload。
- Root Domain overutilized:至少一个 CPU overutilized。
注:原生上是针对Root domain 而言的
Part2:判定overutilized
Mtk hook 后针对perf domain 而言,总利用率超过总容量的 80%,则认定CPU 处于 overutilized 状态,这里有别于原生。
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.如果task 是VIP ,则优先选择 VIP 任务数量最少的 CPU。如果当前CPU 的 VIP 任务数量比之前的最小值更小,则更新候选 CPU 集合。
c++
/*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
如果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 中的数值计算下
js
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
中断关闭导致的延迟
正常片段应该类似如下
在这77ms 时间内,Sf 一直尝试选核到CPU 7上均没有成功,直到70 多ms 后才选了CPU 6.
这段时间内CPU 7上没有收到任何中断,说明中断被关闭,此时CPU 7上运行的线程正在等底层回传buffer
结语:
Android 上之所以采用CFS,单纯因为Android 诞生的时候Linux 已经采用了CFS,慢慢的Google 在上层顺势引入了Cgroup 分组控制等。
但是对于Android 这种强交互系统,某些场景下还不够,所以人们就自然的想到了哪些线程要优先跑,由自己来决定,于是在调度关键路径上的一系列hook 进行客制化,也就是各家宣称的"VIP"、"MVP" 等调度策略,这些的策略的堆叠使得原本已经复杂臃肿的CFS 架构更加复杂,使用不当各种副作用也会接踵而至。
简而言之,资源是一定的,任何策略注定都是顾此失彼,就看这个"失彼" 对用户的影响是否可以做到无感。