代码详细
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;
}
设计意义
预测性调度:通过分析任务的历史行为预测未来需求,使调度器能做出更优决策
负载均衡:帮助系统识别CPU密集型任务,更好地进行负载均衡
能效优化:为CPU频率调节(DVFS)提供依据,在性能和能效间取得平衡
响应时间优化:预测交互式任务的资源需求,提高系统响应性
这种预测机制是现代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; // 更新任务的预测需求值
}
设计意义
预测准确性:只在有意义的事件发生时更新预测值,避免不必要的计算
性能优化:通过条件检查减少不必要的预测计算开销
策略灵活性:通过
sched_class->fixup_walt_sched_stats
允许不同调度类自定义统计修正逻辑实时性保证:对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;
}
作用
该函数的核心逻辑是:
检查任务是否为新任务(刚创建的
fork()
任务),如果是,直接返回当前runtime
(无历史数据可供预测)。分析任务的
busy_buckets
(任务历史负载分桶数据),确定当前负载范围(dmin
和dmax
)。从历史执行时间(
sum_history
)中匹配符合当前负载范围的记录,预测未来需求pred_demand
。返回预测值(需保证不小于当前
runtime
,避免低估需求)。意义
动态负载预测
通过历史负载模式预测任务未来需求,帮助调度器选择更合适的 CPU(如 EAS 节能调度)。避免负载低估
强制预测值≥ runtime
,防止因瞬时低负载导致任务被错误分配到小核。分桶优化效率
使用busy_buckets
快速定位负载范围,减少对历史数据的全量搜索。新任务快速处理
新任务直接使用当前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为止。
"桶增加"算法,主要用于某种热度统计或优先级管理系统
主要功能
选择性增加:对指定索引(idx)的桶进行增加操作,同时衰减其他所有桶的值
动态步长:根据当前值是否超过阈值(CONSISTENT_THRES=16),使用不同的增加步长(INC_STEP=8或INC_STEP_BIG=16)
衰减机制:非目标桶的值会逐步衰减(DEC_STEP=2),直到为0
边界保护:防止桶值溢出(U8_MAX=255)
这种设计能够快速识别出热点项(通过快速增长)同时逐步淘汰不活跃项(通过衰减),在资源管理和调度系统中非常有用。
4.更新 pred_demand
不需要更新的情况有:
- 任务是 idle 任务或任务要出队
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,所以该情况不会发生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)上的预测需求值,主要用于:
评估任务的未来CPU需求
帮助调度器做出更好的调度决策
实现更精准的负载均衡