Linux 6.10 | CPU 调度

374 阅读11分钟

前言

本文基于MTK 平台、kernel 6.1

CPU 调度这块的资料网上一抓一大把,其中很多分析源码细节不错的文章,多是分析CFS 的。 所以本文只是挑了几个场景,画了几个框图以及选了两个案例分析。

唤醒场景下的选核

选核涉及多种不同的场景,这里以fair task 唤醒选核为例进行说明。
无论任务是新创建(通过 fork)还是从阻塞状态唤醒,调度器都会通过 select_task_rq_fair接口进行选核。

image.png

MTK 平台上hook 了#1 及#2,分别用于task_tubro 以及EAS 场景下的选核

注:task_turbo 选核只作用于应用启动环节,所以启动过程中task 只走task_turbo 选核。

Task turbo 选核

image.png

图中可知,task_turbo 的选核是从大核开始选,先优先选idle,然后选剩余计算能力最大的核。

EAS 选核

这里只画了主干路径,一些不常见的路径或feature 强相关的没有体现

select_task_rq_fair.png

注: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:

image.png

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 如下这个片段为例

image.png image.png

之所以最终选择了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 平台上

image.png

其它几种balance,本质都差不多,最终都会触发load_balance 这一函数

案例分析

为便于理解,下面举两个简单的例子

Migration 导致的delay

image.png image.png

简而言之: wmshell.main 线程(本身是vip 线程)被唤醒后经过选核,选到了vip 数最少的核上,但是被紧接着的pull migration 到了一个有13个vip 线程的核上,进而造成一个9ms 的明显deley

中断关闭导致的延迟

image.png image.png

这里可以看到cpu 3向cpu7 发了IPI,但是CPU 7 没有收到

正常片段应该类似如下

image.png

在这77ms 时间内,Sf 一直尝试选核到CPU 7上均没有成功,直到70 多ms 后才选了CPU 6.

image.png

这段时间内CPU 7上没有收到任何中断,说明中断被关闭,此时CPU 7上运行的线程正在等底层回传buffer

Task util 对选核的影响

我们都知道boost/deboost CPU frequency 常见的手段就是垫高task 的util或者削减其util,从而让它变得更大或更小,实际是通过设置sched_setattr 接口进而驱动sugov 调频的。 但实际上,改变util 也会影响选核,这点容易被忽视

这里以一个简短的例子说明下:

image.png

看到这个图,不禁会想: 看起来小核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>