【WALT】频率计算(未更新完)
【WALT】频率计算
代码版本:Linux4.9 android-msm-crosshatch-4.9-android12
@
参考文章:schedutil governor情景分析
一、sugov(schedutil governor)
sugov 在整个调频软件的位置如下所示:
sugov 作为一种内核调频策略模块,它主要是根据当前 CPU 的利用率进行调频。因此,sugov会注册一个 callback 函数(sugov_update_shared/sugov_update_single)到调度器负载跟踪模块,当 CPU util 发生变化的时候就会调用该 callback 函数,检查一下当前 CPU 频率是否和当前的 CPU util 匹配,如果不匹配,那么就进行提频或者降频。
为了适配各种场景,sugov 还提供了可调参数,用户空间可以检测当前的场景,并根据不同的场景设定不同的参数,以便满足用户性能/功耗的需求。
sugov 选定 target frequency 之后,需要通过 cpufreq core(cpufreq framework)、cpufreq driver,cpu 调频硬件完成频率的调整。cpufreq core 是一个硬件无关的调频框架,集中管理了 cpufreq governor、cpufreq driver、cpufreq device 对象,同时提供了简单方便使用的接口 API,让工程师很轻松的就能完成特定 governor 或者 driver 的撰写。
以上内容来自:schedutil governor情景分析
我们先来看看 sugov 的注册。
<kernel/sched/cpufreq_schedutil.c>
static struct cpufreq_governor schedutil_gov = {
.name = "schedutil",
.owner = THIS_MODULE,
.init = sugov_init,
.exit = sugov_exit,
.start = sugov_start,
.stop = sugov_stop,
.limits = sugov_limits,
};
在这里,我们更加关注 sugov 的启动。
<kernel/sched/cpufreq_schedutil.c>
static int sugov_start(struct cpufreq_policy *policy)
{
struct sugov_policy *sg_policy = policy->governor_data;
unsigned int cpu;
sg_policy->up_rate_delay_ns =
sg_policy->tunables->up_rate_limit_us * NSEC_PER_USEC;
sg_policy->down_rate_delay_ns =
sg_policy->tunables->down_rate_limit_us * NSEC_PER_USEC;
update_min_rate_limit_us(sg_policy);
sg_policy->last_freq_update_time = 0;
sg_policy->next_freq = UINT_MAX;
sg_policy->work_in_progress = false;
sg_policy->need_freq_update = false;
sg_policy->cached_raw_freq = 0;
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
memset(sg_cpu, 0, sizeof(*sg_cpu));
sg_cpu->sg_policy = sg_policy;
sg_cpu->cpu = cpu;
sg_cpu->flags = SCHED_CPUFREQ_RT;
sg_cpu->iowait_boost_max = policy->cpuinfo.max_freq;
}
for_each_cpu(cpu, policy->cpus) {
struct sugov_cpu *sg_cpu = &per_cpu(sugov_cpu, cpu);
cpufreq_add_update_util_hook(cpu, &sg_cpu->update_util,
policy_is_shared(policy) ?
sugov_update_shared :
sugov_update_single);
}
return 0;
}
在启动时,调用 cpufreq_add_update_util_hook()
注册回调函数。注册之后,就可以在内核中通过 cpufreq_update_util()
执行回调函数计算频率。
<kernel/sched/cpufreq.c>
DECLARE_PER_CPU(struct update_util_data *, cpufreq_update_util_data);
void cpufreq_add_update_util_hook(int cpu, struct update_util_data *data,
void (*func)(struct update_util_data *data, u64 time,
unsigned int flags))
{
if (WARN_ON(!data || !func))
return;
if (WARN_ON(per_cpu(cpufreq_update_util_data, cpu)))
return;
data->func = func;
rcu_assign_pointer(per_cpu(cpufreq_update_util_data, cpu), data);
}
EXPORT_SYMBOL_GPL(cpufreq_add_update_util_hook);
<kernel/sched/sched.h>
DECLARE_PER_CPU(struct update_util_data *, cpufreq_update_util_data);
struct update_util_data {
void (*func)(struct update_util_data *data, u64 time, unsigned int flags);
};
static inline void cpufreq_update_util(struct rq *rq, unsigned int flags)
{
struct update_util_data *data;
data = rcu_dereference_sched(*per_cpu_ptr(&cpufreq_update_util_data,
cpu_of(rq)));
if (data)
data->func(data, ktime_get_ns(), flags);
}
通过 data->func(data, ktime_get_ns(), flags)
就可以调用以下两个函数之一:
- static void
sugov_update_shared
(struct update_util_data *hook, u64 time, unsigned int flags)
用于拥有多个 CPU 的簇 - static void
sugov_update_single
(struct update_util_data *hook, u64 time, unsigned int flags)
用于单个 CPU(如超大核)
其中,flag 如下:
<include/linux/sched.h>
#define SCHED_CPUFREQ_RT (1U << 0)
#define SCHED_CPUFREQ_DL (1U << 1)
#define SCHED_CPUFREQ_IOWAIT (1U << 2)
#define SCHED_CPUFREQ_INTERCLUSTER_MIG (1U << 3)
#define SCHED_CPUFREQ_RESERVED (1U << 4)
#define SCHED_CPUFREQ_PL (1U << 5)
#define SCHED_CPUFREQ_EARLY_DET (1U << 6)
#define SCHED_CPUFREQ_FORCE_UPDATE (1U << 7)
#define SCHED_CPUFREQ_RT_DL (SCHED_CPUFREQ_RT | SCHED_CPUFREQ_DL)
二、计算时机
有两种方法可以执行回调函数:
- 直接调用
cpufreq_update_util()
执行 - 通过
cpufreq_update_this_cpu()
执行
我们先来看看 cpufreq_update_this_cpu()
:
static inline void cpufreq_update_this_cpu(struct rq *rq, unsigned int flags)
{
if (cpu_of(rq) == smp_processor_id())
cpufreq_update_util(rq, flags);
}
这个函数由正在更新利用率的 CPU 上的调度程序调用。它主要由 CFS、RT 和 DL 调度类使用。它本质上是一种补丁。
两种方法本质上都是调用 cpufreq_update_util()
执行。
1. 直接调用 cpufreq_update_util()
执行
-
try_to_wake_up()
:唤醒任务时如果任务状态与给定 state 不同,则不唤醒任务且调频<kernel/sched/core.c> static int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags) { …… if (!(p->state & state)) goto out; out: if (success && sched_predl) { raw_spin_lock_irqsave(&cpu_rq(cpu)->lock, flags); if (do_pl_notif(cpu_rq(cpu))) cpufreq_update_util(cpu_rq(cpu), SCHED_CPUFREQ_PL); raw_spin_unlock_irqrestore(&cpu_rq(cpu)->lock, flags); } …… }
-
scheduler_tick()
:在 tick 到达的时候,如果当前运行队列的 cfs 任务中有一个能作为 ed_task(early detection task),就调频<kernel/sched/core.c> void scheduler_tick(void) { …… early_notif = early_detection_notify(rq, wallclock); if (early_notif) cpufreq_update_util(rq, SCHED_CPUFREQ_EARLY_DET); …… }
我们来看看 ed_task 是什么,从
early_detection_notify()
开始。<kernel/sched/walt.c> #define EARLY_DETECTION_DURATION 9500000 bool early_detection_notify(struct rq *rq, u64 wallclock) { struct task_struct *p; int loop_max = 10; if ((!walt_rotation_enabled && sched_boost_policy() == SCHED_BOOST_NONE) || !rq->cfs.h_nr_running) return 0; rq->ed_task = NULL; list_for_each_entry(p, &rq->cfs_tasks, se.group_node) { if (!loop_max) break; if (wallclock - p->last_wake_ts >= EARLY_DETECTION_DURATION) { rq->ed_task = p; return 1; } loop_max--; } return 0; }
其中,
if (wallclock - p->last_wake_ts >= EARLY_DETECTION_DURATION)
是重点。p->last_wake_ts
是任务被唤醒的时间,该语句的意思是:如果当前时间与任务被唤醒时间的差超过了 9.5 ms,那么该任务就是当前运行队列的 ed_task。 -
walt_irq_work()
:void walt_irq_work(struct irq_work *irq_work) { …… for_each_sched_cluster(cluster) { u64 aggr_grp_load = 0; raw_spin_lock(&cluster->load_lock); for_each_cpu(cpu, &cluster->cpus) { rq = cpu_rq(cpu); if (rq->curr) { update_task_ravg(rq->curr, rq, TASK_UPDATE, wc, 0); account_load_subtractions(rq); aggr_grp_load += rq->grp_time.prev_runnable_sum; } } cluster->aggr_grp_load = aggr_grp_load; raw_spin_unlock(&cluster->load_lock); } for_each_sched_cluster(cluster) { for_each_cpu(cpu, &cluster->cpus) { int nflag = 0; rq = cpu_rq(cpu); if (is_migration) { if (rq->notif_pending) { nflag = SCHED_CPUFREQ_INTERCLUSTER_MIG; rq->notif_pending = false; } else { nflag = SCHED_CPUFREQ_FORCE_UPDATE; } } cpufreq_update_util(rq, nflag); } } …… }
在该函数中,会遍历所有簇中的所有 CPU(代码展示的第二个 for 循环),为他们计算一次频率。
如果我们追根溯源,就会发现调用该函数的源头竟然在开机时的初始化中:
start_kernel() -> sched_init() -> walt_sched_init() -> walt_irq_work() -> cpufreq_update_util()
其中,walt_sched_init() 会注册回调函数:
<kernel/sched/walt.c> void walt_sched_init(struct rq *rq) { …… init_irq_work(&walt_migration_irq_work, walt_irq_work); init_irq_work(&walt_cpufreq_irq_work, walt_irq_work); …… }
这样,每次执行
irq_work_queue(&walt_cpufreq_irq_work)
或irq_work_queue(&walt_migration_irq_work)
时,就会将这两个 irq_work enqueue 到当前 CPU 上,然后触发一个 IPI 中断,在 IPI 中断中调用cpufreq_update_util()
执行调频的回调函数。-
在执行
run_walt_irq_work()
时会执行irq_work_queue(&walt_cpufreq_irq_work)
,run_walt_irq_work()
在执行 WALT入口 update_task_ravg() 时会被调用。(也就是说,在代码展示的第一个 for 循环中,也会计算一次频率。)
-
在执行
fixup_busy_time()
时会执行irq_work_queue(&walt_migration_irq_work)
。fixup_busy_time()
在任务迁核时调整负载,调整后会判断是否计算频率,具体可以点击查看【WALT】update_cpu_busy_time() 代码详解 。
-
-
cfs_rq_util_change()
:如果当前的 cfs_rq 就是当前 CPU 运行队列的 cfs_rq,就计算频率<kernel/sched/fair.c> static inline void cfs_rq_util_change(struct cfs_rq *cfs_rq) { if (&this_rq()->cfs == cfs_rq) { cpufreq_update_util(rq_of(cfs_rq), 0); } }
这个函数在调度实体(se,sched_entity)被进行以下操作时使用:
- update_cfs_rq_load_avg()
- attach_entity_load_avg()
- detach_entity_load_avg()
-
update_load_avg()
:直接计算频率,但是在当前版本中不使用<kernel/sched/fair.c> static inline void update_load_avg(struct sched_entity *se, int not_used1) { cpufreq_update_util(rq_of(cfs_rq_of(se)), 0); }
2. 通过 cpufreq_update_this_cpu()
执行
-
enqueue_task_fair()
:static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) { …… if (p->in_iowait) cpufreq_update_this_cpu(rq, SCHED_CPUFREQ_IOWAIT); …… for_each_sched_entity(se) { …… update_load_avg(se, UPDATE_TG); } …… }
enqueue_task_fair() 是 CFS 调度类对应的 enqueue_task() 的方法函数。
在任务发生 iowait 的时候,内核会将其 in_iowait 设置为 1。如果当前入队的任务出现这种情况,就通过 cpufreq_update_this_cpu() 计算一次频率。
值得注意的是,enqueue_task_fair() 中也会调用 update_load_avg(),但此处并非上文定义的 update_load_avg()。
-
update_curr_rt()
static void update_curr_rt(struct rq *rq) { …… cpufreq_update_this_cpu(rq, SCHED_CPUFREQ_RT); }
-
update_curr_dl()
static void update_curr_dl(struct rq *rq) { …… cpufreq_update_this_cpu(rq, SCHED_CPUFREQ_RT); }
我们可以发现,在该版本代码中,主要由 CFS 调度类调用计算频率的回调函数,因为绝大部分任务都是 cfs 任务。
cpufreq_update_this_cpu()
中描述的第 2 和 3 种情况主要是为了避免 RT 和 DL 任务长期占据 CPU 导致 CFS 调度类无法定时调用计算频率回调函数的情况而打上的补丁。如果 CPU 上长期执行 RT 和 DL 任务,那么只有在任务被唤醒、上下 CPU 或 tick 抵达等情况时计算频率,他们之间间隔的时间可能会大于等于 4ms。
三、计算流程
频率的计算从负载的计算开始,由于【WALT】调度与负载计算 中已经介绍了负载的计算,我们便跳过这一步,从 freq_policy_load()
开始。
1. freq_policy_load()
<kernel/sched/walt.c>
u64 freq_policy_load(struct rq *rq)
{
unsigned int reporting_policy = sysctl_sched_freq_reporting_policy;
int freq_aggr_thresh = sched_freq_aggregate_threshold;
struct sched_cluster *cluster = rq->cluster;
u64 aggr_grp_load = cluster->aggr_grp_load;
u64 load, tt_load = 0;
if (rq->ed_task != NULL) {
load = sched_ravg_window;
goto done;
}
if (aggr_grp_load > freq_aggr_thresh)
load = rq->prev_runnable_sum + aggr_grp_load;
else
load = rq->prev_runnable_sum + rq->grp_time.prev_runnable_sum;
tt_load = top_task_load(rq);
switch (reporting_policy) {
case FREQ_REPORT_MAX_CPU_LOAD_TOP_TASK:
load = max_t(u64, load, tt_load);
break;
case FREQ_REPORT_TOP_TASK:
load = tt_load;
break;
case FREQ_REPORT_CPU_LOAD:
break;
default:
break;
}
done:
trace_sched_load_to_gov(rq, aggr_grp_load, tt_load, freq_aggr_thresh,
load, reporting_policy, walt_rotation_enabled);
return load;
}
该函数是用来预测 CPU 的负载的,函数的返回值 load 于 CPU 的含义相当于 demand 于 task 的含义。
-
ed_task
ed_task 的概念在上文提到。如果 CPU 运行队列中存在 ed_task,直接将窗口大小作为 load 返回。
-
aggr_grp_load
在上文提到的
walt_irq_work()
中,除了为每个 CPU 都计算一次频率,还会为每个簇计算一次 aggr_grp_load。<kernel/sched/walt.c> void walt_irq_work(struct irq_work *irq_work) { …… for_each_sched_cluster(cluster) { u64 aggr_grp_load = 0; raw_spin_lock(&cluster->load_lock); for_each_cpu(cpu, &cluster->cpus) { rq = cpu_rq(cpu); if (rq->curr) { update_task_ravg(rq->curr, rq, TASK_UPDATE, wc, 0); account_load_subtractions(rq); aggr_grp_load += rq->grp_time.prev_runnable_sum; } } cluster->aggr_grp_load = aggr_grp_load; raw_spin_unlock(&cluster->load_lock); } …… }
其中,
rq->grp_time.prev_runnable_sum
在【WALT】update_cpu_busy_time() 代码详解 中和 【WALT】调度与负载计算 中都有介绍,在此不再赘述。 -
tt_load
tt_load 的计算在【WALT】top task 相关代码详解 中有描述,在此不再赘述。
-
reporting_policy
reporting_policy = sysctl_sched_freq_reporting_policy,这个参数默认为0。
#define FREQ_REPORT_MAX_CPU_LOAD_TOP_TASK 0 #define FREQ_REPORT_CPU_LOAD 1 #define FREQ_REPORT_TOP_TASK 2
也就是说,freq_policy_load()
的结果是 max(load, tt_load),是根据 update_cpu_busy_time() 的结果计算出来的一个预测值,在后文中我们称之为 load。
2. cpu_util_freq_walt()
static inline unsigned long
cpu_util_freq_walt(int cpu, struct sched_walt_cpu_load *walt_load)
{
u64 util, util_unboosted;
struct rq *rq = cpu_rq(cpu);
unsigned long capacity = capacity_orig_of(cpu);
int boost;
if (walt_disabled || !sysctl_sched_use_walt_cpu_util)
return cpu_util_freq_pelt(cpu);
boost = per_cpu(sched_load_boost, cpu);
util_unboosted = util = freq_policy_load(rq);
util = div64_u64(util * (100 + boost),
walt_cpu_util_freq_divisor);
if (walt_load) {
u64 nl = cpu_rq(cpu)->nt_prev_runnable_sum +
rq->grp_time.nt_prev_runnable_sum;
u64 pl = rq->walt_stats.pred_demands_sum;
/* do_pl_notif() needs unboosted signals */
rq->old_busy_time = div64_u64(util_unboosted,
sched_ravg_window >>
SCHED_CAPACITY_SHIFT);
rq->old_estimated_time = div64_u64(pl, sched_ravg_window >>
SCHED_CAPACITY_SHIFT);
nl = div64_u64(nl * (100 + boost),
walt_cpu_util_freq_divisor);
pl = div64_u64(pl * (100 + boost),
walt_cpu_util_freq_divisor);
walt_load->prev_window_util = util;
walt_load->nl = nl;
walt_load->pl = pl;
walt_load->ws = walt_load_reported_window;
}
return (util >= capacity) ? capacity : util;
}
freq_policy_load()
只在 cpu_util_freq_walt()
中执行,如果没有开启 WALT,就走 cpu_util_freq_pelt()
,在此不多赘述。
参数介绍:
-
capacity
capacity = capacity_orig_of(cpu)
,是当前 CPU 的最大算力 max_cap。 -
boost
boost = per_cpu(sched_load_boost, cpu)
,是每个 CPU 的 sched_load_boost。在该版本中 sched_load_boost 默认为 0,但是可以手动调整。代码中对该值的描述如下:/* * -100 is low enough to cancel out CPU's load and make it near zro. * 1000 is close to the maximum value that cpu_util_freq_{walt,pelt} * can take without overflow. */
-
util_unboosted
util_unboosted = freq_policy_load(rq)
,就是当前 CPU 根据freq_policy_load()
算出来的 load。 -
util
开始时
util = freq_policy_load(rq)
,然后进行一次除法运算:
\(util = load * \dfrac{100 + boost}{walt\_cpu\_util\_freq\_divisor}\)其中,
walt_cpu_util_freq_divisor = (sched_ravg_window >> SCHED_CAPACITY_SHIFT) * 100
,SCHED_CAPACITY_SHIFT = 10,boost = sched_load_boost,于是:
\(util =1024\times \dfrac{load}{sched\_ravg\_window}\times\dfrac{100+sched\_load\_boost}{100}\)
最终,返回 (util >= capacity) ? capacity : util
。也就是说,返回的是:
\(min(max\_cap,1024\times \dfrac{load}{sched\_ravg\_window}\times\dfrac{100+sched\_load\_boost}{100})\)
在该函数中,还进行了部分信息的更新,如:
-
walt_load->prev_window_util
prev_window_util 就是刚刚计算出来的 util(并非是 laod)。
-
walt_load->nl
nl 全称为 new task load,是根据运行队列的 nt_prev_runnable_sum 以及运行队列的相关线程组的 nt_prev_runnable_sum 累加而成,此处多进行了一次除法运算:
\(nl = 1024\times \dfrac{nl}{sched\_ravg\_window}\times\dfrac{100+sched\_load\_boost}{100}\)runnable_sum 的具体细节可以看【WALT】update_cpu_busy_time() 代码详解 和 【WALT】调度与负载计算 。
-
walt_load->pl
pl 全称为 predict task load,是运行队列的 pred_demands_sum,此处多进行了一次除法运算:
\(pl = 1024\times \dfrac{pl}{sched\_ravg\_window}\times\dfrac{100+sched\_load\_boost}{100}\)pred_demands_sum 的具体细节可以看【WALT】调度与负载计算 。
-
walt_load->ws
ws 是 walt_load_reported_window,是最近的窗口开始的时间:
walt_load_reported_window = atomic64_read(&walt_irq_work_lastq_ws)
atomic64_set(&walt_irq_work_lastq_ws, rq->window_start)
-
rq->old_busy_time
old_busy_time 是归一化到 1024 的 load 值。
\(old\_busy\_time = 1024 \times \dfrac{load}{sched\_ravg\_window}\) -
rq->old_estimated_time
old_estimated_time 是归一化到 1024 的原始 pl 值。
\(old\_estimated\_time = 1024 \times \dfrac{pl}{sched\_ravg\_window}\)
3. cpu_util_freq()
#ifdef CONFIG_SCHED_WALT
static inline unsigned long
cpu_util_freq(int cpu, struct sched_walt_cpu_load *walt_load)
{
return cpu_util_freq_walt(cpu, walt_load);
}
#else
static inline unsigned long
cpu_util_freq(int cpu, struct sched_walt_cpu_load *walt_load)
{
return cpu_util_freq_pelt(cpu);
}
跟 cpu_util_freq_walt()
一样,只是 cpu_util_freq()
是一个入口,根据开启 WALT 或开启 PELT 来选择执行不同的函数。
4. boosted_cpu_util()
unsigned long
boosted_cpu_util(int cpu, struct sched_walt_cpu_load *walt_load)
{
unsigned long util = cpu_util_freq(cpu, walt_load);
long margin = schedtune_cpu_margin(util, cpu);
trace_sched_boost_cpu(cpu, util, margin);
return util + margin;
}
5. sugov_get_util()
static void sugov_get_util(unsigned long *util, unsigned long *max, int cpu)
{
struct rq *rq = cpu_rq(cpu);
unsigned long cfs_max;
struct sugov_cpu *loadcpu = &per_cpu(sugov_cpu, cpu);
cfs_max = arch_scale_cpu_capacity(NULL, cpu);
*util = min(rq->cfs.avg.util_avg, cfs_max);
*max = cfs_max;
*util = boosted_cpu_util(cpu, &loadcpu->walt_load);
}