【WALT】predict_and_update_buckets() 与 update_task_pred_demand() 代码详解

代码详细

static inline u32 predict_and_update_buckets(struct rq *rq,
			struct task_struct *p, u32 runtime) {

	int bidx;
	u32 pred_demand;

	if (!sched_predl)
		return 0;

	// ⑴ 根据 runtime 给出桶的下标
	bidx = busy_to_bucket(runtime);
	// ⑵ 根据桶的下标预测 pred_demand
	pred_demand = get_pred_busy(rq, p, bidx, runtime);
	// ⑶ 根据桶的下标更新桶
	bucket_increase(p->ravg.busy_buckets, bidx);

	return pred_demand;
}

设计意义

  1. 预测性调度:通过分析任务的历史行为预测未来需求,使调度器能做出更优决策

  2. 负载均衡:帮助系统识别CPU密集型任务,更好地进行负载均衡

  3. 能效优化:为CPU频率调节(DVFS)提供依据,在性能和能效间取得平衡

  4. 响应时间优化:预测交互式任务的资源需求,提高系统响应性

这种预测机制是现代Linux调度器(如EAS)的重要组成部分,特别是在移动设备和嵌入式系统中,对平衡性能和功耗至关重要。


void update_task_pred_demand(struct rq *rq, struct task_struct *p, int event)
{
	u32 new, old;
    if (!sched_predl) return;  // 预测调度功能未启用则直接返回
    if (is_idle_task(p) || exiting_task(p)) return;  // 空闲任务或正在退出的任务不处理

	if (event != PUT_PREV_TASK && event != TASK_UPDATE &&
			(!SCHED_FREQ_ACCOUNT_WAIT_TIME ||
			 (event != TASK_MIGRATE &&
			 event != PICK_NEXT_TASK)))
		return;

	if (event == TASK_UPDATE) {
		if (!p->on_rq && !SCHED_FREQ_ACCOUNT_WAIT_TIME)
			return;
	}

    new = calc_pred_demand(rq, p);  // 计算新的预测需求值
    old = p->ravg.pred_demand;     // 获取旧的预测值
    // 新值不大于旧值则不更新
	if (old >= new)
		return;

	if (task_on_rq_queued(p) && (!task_has_dl_policy(p) ||
				!p->dl.dl_throttled))
		p->sched_class->fixup_walt_sched_stats(rq, p, p->ravg.demand, new);

	p->ravg.pred_demand = new;  // 更新任务的预测需求值
}

设计意义

  1. 预测准确性:只在有意义的事件发生时更新预测值,避免不必要的计算

  2. 性能优化:通过条件检查减少不必要的预测计算开销

  3. 策略灵活性:通过sched_class->fixup_walt_sched_stats允许不同调度类自定义统计修正逻辑

  4. 实时性保证:对deadline任务特殊处理,确保实时性要求不被破坏

这段代码是Linux调度器实现智能预测调度的关键部分,特别是在移动设备调度器(如EAS)中,它帮助系统:

  • 更准确地预测任务资源需求

  • 做出更好的调度决策

  • 实现更有效的CPU频率调节

  • 平衡性能和能效

代码逻辑

sched_predl默认值为1,意味着正常情况下这两个函数都会执行。

predict_and_update_buckets() 主要通过桶算法来预测 pred_demand。其中,runtime 是在 update_task_demand() 中根据 scale_exec_time() 归一化过后的执行时间。

update_task_pred_demand() 主要是在任务的 perd_demand 值小于 curr_window 时再次计算 pred_demand。

桶算法的核心是数组 busy_buckets[NUM_BUSY_BUCKETS]

代码中对数组busy_bucketsd 描述是:"busy_buckets"将历史busy time分组到用于预测的不同桶中。('busy_buckets'groups historical busy time into different buckets used for prediction.)

数组busy_buckets是一种类似PELT的机制,数组中每一个下标对应的值可以算作是一种权重,数组会根据窗口的不断翻滚对权重进行衰减或加强。

桶算法将一个WALT窗口的时间分为 NUM_BUSY_BUCKETS 块(默认为 10 块),可以看做是 NUM_BUSY_BUCKETS 个等级。runtime约到,它所处的等级就越高。

每次调用函数 predict_and_update_buckets() 时会先判断上一个窗口中当前任务所执行的时间(归一化后)处于的等级bidx,根据bidx 和 数组 busy_buckets 来预测 pred_demand,然后对数据 busy_buckets 中下标非 bidx 的权重进行衰减,对下标为 bidx 的权重进行加强。

1.根据 runtime 给出桶的下标

static inline int busy_to_bucket(u32 normalized_rt)
{
	int bidx;

	bidx = mult_frac(normalized_rt, NUM_BUSY_BUCKETS, max_task_load());
	bidx = min(bidx, NUM_BUSY_BUCKETS - 1);

	if (!bidx)
		bidx++;

	return bidx;
}

参数解释:

  • normalized_rt:传入的 runtime(已归一化);
  • NUM_BUSY_BUCKETS:设定的桶的数量,是数组 busy_buckets[NUM_BUSY_BUCKETS] 的长度,当前版本中默认为 10;
  • max_task_load():函数返回值为窗口大小 sched_ravg_window。

因此可得bidx = NUM_BUSY_BUCKETS * (runtime / window_size)

为了防止bidx超出数组 busy_buckets 的下标,还需要再bidx和 NUM_BUSY_BUCKETS - 1中取最小值。

如果得到的bidx 为 0,就让bidx = 1.这是因为最低频率落入第二个桶,因此继续预测最低的桶是没有用的。(源自代码注释:The lowest frequency falls into 2nd bucket and thus keep predicting lowest bucket is not useful.)函数返回下标 bidx,用于 pred_demand 的计算当中。

意义:

  • 分桶(Bucketing):将连续的任务负载值离散化到有限的桶中,便于调度器快速分类和处理任务。

    • 例如:低负载、中负载、高负载任务可以分别映射到不同的桶。

  • 性能优化:通过分桶,调度器可以基于桶的统计信息(如平均负载)快速决策,而无需处理原始值。

  • 边界处理:避免空负载或极小负载干扰调度逻辑(通过跳过 0 号桶)。

典型应用场景:

  • 在 Linux 内核的调度器(如 CFS)中,可能用于将任务的 CPU 使用率或运行时间分类到不同的负载组。

  • 在负载均衡算法中,帮助快速识别高负载任务以便迁移或分配资源。

2.根据桶的下标预测 pred_demand

static u32 get_pred_busy(struct rq *rq, struct task_struct *p,
				int start, u32 runtime)
{
	int i;
	u8 *buckets = p->ravg.busy_buckets;
	u32 *hist = p->ravg.sum_history;
	u32 dmin, dmax;
	u64 cur_freq_runtime = 0;
	int first = NUM_BUSY_BUCKETS, final;
	u32 ret = runtime;
	u32 temp = runtime;

	// 1. 如果任务刚被创建,直接结束
	if (unlikely(is_new_task(p)))
		goto out;

	// 2. 根据下标 bidx 和 busy_buckets 数组找到上下界 dmin 和 dmax
	for (i = start; i < NUM_BUSY_BUCKETS; i++) {
		if (buckets[i]) {
			first = i;
			break;
		}
	}
	if (first >= NUM_BUSY_BUCKETS)
		goto out;

	final = first;

	if (final < 2) {
		dmin = 0;
		final = 1;
	} else {
		dmin = mult_frac(final, max_task_load(), NUM_BUSY_BUCKETS);
	}
	dmax = mult_frac(final + 1, max_task_load(), NUM_BUSY_BUCKETS);

	// 3. 根据 dmin 和 dmax,以及历史执行时间来预测 pred_demand
	for (i = 0; i < sched_ravg_hist_size; i++) {
		if (hist[i] >= dmin && hist[i] < dmax) {
			ret = hist[i];
			break;
		}
	}
	if (ret < dmin)
		ret = (dmin + dmax) / 2;
	ret = max(runtime, ret);
out:
	trace_sched_update_pred_demand(rq, p, runtime,
		mult_frac((unsigned int)cur_freq_runtime, 100,
			  sched_ravg_window), ret);
	return ret;
}

作用

该函数的核心逻辑是:

  1. 检查任务是否为新任务(刚创建的 fork() 任务),如果是,直接返回当前 runtime(无历史数据可供预测)。

  2. 分析任务的 busy_buckets(任务历史负载分桶数据),确定当前负载范围(dmin 和 dmax)。

  3. 从历史执行时间(sum_history)中匹配符合当前负载范围的记录,预测未来需求 pred_demand

  4. 返回预测值(需保证不小于当前 runtime,避免低估需求)。 

意义

  1. 动态负载预测

    通过历史负载模式预测任务未来需求,帮助调度器选择更合适的 CPU(如 EAS 节能调度)。
  2. 避免负载低估

    强制预测值 ≥ runtime,防止因瞬时低负载导致任务被错误分配到小核。
  3. 分桶优化效率

    使用 busy_buckets 快速定位负载范围,减少对历史数据的全量搜索。
  4. 新任务快速处理

    新任务直接使用当前 runtime,避免无效历史查询。

这段代码是调度器动态预测任务负载的核心逻辑,通过结合历史分桶数据和实时运行时间,平衡短期波动与长期趋势,为任务调度提供更智能的决策依据。

get_pred_busy()函数根据runtime 和 桶洗下标 bidx 来找到一个 busy time。

1.如果任务刚被创建,直接结束

新创建的任务没有历史执行时间的数据,因此没有办法进行预测,直接结束,返回runtime。

2.根据下表 bidx 和 数组 busy_buckets 找到山下界 dmin 和 dmax

数组 busy_buckets 中值非 0 意味着至少 4 个历史窗口内有某个窗口的 runtime 处于该等级。

遍历数组 busy_buckets 是如果发现第一个非 0 的值,就把该值所在的下标作为 first,并让 final = first。

如果final < 2,就意味着过去的4个历史窗口中曾经有一个窗口的 runtime 处于等级 0 或 1,就让 dmin = 0,dmax = window_size * (2 / NUM_BUSY_BUCKETS)。

如果 final >= 2,就让dmin = window_size * (final / NUM_BUSY_BUCKETS);dmax = window_size * ((final + 1) / NUM_BUSY_BUCKETS)。

3.根据dmin 和 dmax,以及历史执行时间来预测pred_demand

ret的初始值设置为runtime。得到上下界 dmin 和 dmax 之后,遍历数组 sum_history,看看保存的历史窗口的时间中是否有处于该上下界之间的,如果有就将ret设置为该时间,没有的话 ret 的值仍为runtime。

如果 ret 的值比上界 dmin 还小,意味着 runtime 比之前的所有历史事件都小,就让 ret 的值设置为(dmin + dmax) / 2。

最后再在runtime 和 ret 之间取最大值,作为 pred_demand 返回。

3.根据桶的下标更新桶

#define INC_STEP 8
#define DEC_STEP 2
#define CONSISTENT_THRES 16
#define INC_STEP_BIG 16
static inline void bucket_increase(u8 *buckets, int idx)
{
	int i, step;
	for (i = 0; i < NUM_BUSY_BUCKETS; i++) {
		// 如果下标不是 bidx,衰减直至0
		if (idx != i) {
			if (buckets[i] > DEC_STEP)
				buckets[i] -= DEC_STEP;
			else
				buckets[i] = 0;
		} else {
			step = buckets[i] >= CONSISTENT_THRES ?
						INC_STEP_BIG : INC_STEP;
			// 	U8_MAX = 255
			if (buckets[i] > U8_MAX - step)
				buckets[i] = U8_MAX;
			else
				buckets[i] += step;
		}
	}
}

这一部分就是根据 bidx 更新数组 busy_buckets。

busy_buckets[bidx]的值如果大于等于16,就累加16,直至255;如果小于16,就累加8,直至255。

下标不是 bidx 的值都要衰减 2,直接该值为0为止。

"桶增加"算法,主要用于某种热度统计或优先级管理系统

主要功能

  1. 选择性增加:对指定索引(idx)的桶进行增加操作,同时衰减其他所有桶的值

  2. 动态步长:根据当前值是否超过阈值(CONSISTENT_THRES=16),使用不同的增加步长(INC_STEP=8或INC_STEP_BIG=16)

  3. 衰减机制:非目标桶的值会逐步衰减(DEC_STEP=2),直到为0

  4. 边界保护:防止桶值溢出(U8_MAX=255)

这种设计能够快速识别出热点项(通过快速增长)同时逐步淘汰不活跃项(通过衰减),在资源管理和调度系统中非常有用。

4.更新 pred_demand

不需要更新的情况有:

  1. 任务是 idle 任务或任务要出队
  2. if (event != PUT_PREV_TASK && event != TASK_UPDATE && (!SCHED_FREQ_ACCOUNT_WAIT_TIME || (event != TASK_MIGRATE && event != PICK_NEXT_TASK)))
    但是当前版本中 SCHED_FREQ_ACCOUNT_WAIT_TIME 默认为 1,所以该情况不会发生
  3. if (event == TASK_UPDATE) and if (!p->on_rq && !SCHED_FREQ_ACCOUNT_WAIT_TIME)
    同上,不会发生

需要更新时,执行函数 calc_pred_demand():

static inline u32 calc_pred_demand(struct rq *rq, struct task_struct *p)
{
    // 如果当前预测值已经大于等于当前窗口的实际需求
    if (p->ravg.pred_demand >= p->ravg.curr_window)
        return p->ravg.pred_demand; // 直接返回现有预测值

    // 否则重新计算预测需求
    return get_pred_busy(rq, p, busy_to_bucket(p->ravg.curr_window),
             p->ravg.curr_window);
}

如果任务的pred_demand 比 curr_window 大,就不更新,否则重新通过 get_pred_busy() 更新 pred_demand。

其中,curr_window 在 update_cpu_busy_time() 中计算,并不在 update_task_demand() 中计算。

最后,如果任务在运行队列上,且任务不是 dl 任务或 dl 任务的时间没有耗尽,就通过 fixup_walt_sched_stats() 计算负载。 【WALT】调度与负载计算

该函数计算一个任务(p)在当前运行队列(rq)上的预测需求值,主要用于:

  1. 评估任务的未来CPU需求

  2. 帮助调度器做出更好的调度决策

  3. 实现更精准的负载均衡

点击此处回到 WALT 入口函数 update_task_ravg()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值