【WALT】频率计算(未更新完)

发布时间 2024-01-06 18:07:50作者: Cyrusandy

【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)

二、计算时机

有两种方法可以执行回调函数:

  1. 直接调用 cpufreq_update_util() 执行
  2. 通过 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() 执行

  1. 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);
    	}
    	……
    }
    
  2. 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。

  3. 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() 代码详解

  4. 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()
  5. 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() 执行

  1. 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()。

  2. update_curr_rt()

    static void update_curr_rt(struct rq *rq)
    {
    	……
    	cpufreq_update_this_cpu(rq, SCHED_CPUFREQ_RT);
    }
    
  3. 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 的含义。

  1. ed_task

    ed_task 的概念在上文提到。如果 CPU 运行队列中存在 ed_task,直接将窗口大小作为 load 返回。

  2. 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】调度与负载计算 中都有介绍,在此不再赘述。

  3. tt_load

    tt_load 的计算在【WALT】top task 相关代码详解 中有描述,在此不再赘述。

  4. 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(),在此不多赘述。

参数介绍:

  1. capacity

    capacity = capacity_orig_of(cpu),是当前 CPU 的最大算力 max_cap。

  2. 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.
     */
    
  3. util_unboosted

    util_unboosted = freq_policy_load(rq),就是当前 CPU 根据 freq_policy_load() 算出来的 load。

  4. 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);
}