示例代码
void update_task_ravg(struct task_struct *p, struct rq *rq, int event,
u64 wallclock, u64 irqtime) {
u64 old_window_start;
// ⑴ 判断是否进入 WALT 算法
if (!rq->window_start || sched_disable_window_stats ||
p->ravg.mark_start == wallclock)
return;
lockdep_assert_held(&rq->lock);
// ⑵ 获取 WALT 算法中上一个窗口的开始时间
old_window_start = update_window_start(rq, wallclock, event);
// ⑶ 如果任务刚初始化结束,不进入 WALT 算法,进入 `done`
if (!p->ravg.mark_start) {
update_task_cpu_cycles(p, cpu_of(rq), wallclock);
goto done;
}
// ⑷ 更新任务及 CPU 的 cycles
update_task_rq_cpu_cycles(p, rq, event, wallclock, irqtime);
// ⑸ 更新任务及 CPU 的 demand 及 pred_demand
update_task_demand(p, rq, event, wallclock);
// ⑹ 更新 CPU 的 busy time
update_cpu_busy_time(p, rq, event, wallclock, irqtime);
// ⑺ 更新任务的 pred_demand
update_task_pred_demand(rq, p, event);
// ⑻ 如果任务正在退出,进入 `done`
if (exiting_task(p))
goto done;
// 两个系统自带的 tracepoint
trace_sched_update_task_ravg(p, rq, event, wallclock, irqtime,
rq->cc.cycles, rq->cc.time, &rq->grp_time);
trace_sched_update_task_ravg_mini(p, rq, event, wallclock, irqtime,
rq->cc.cycles, rq->cc.time, &rq->grp_time);
done:
p->ravg.mark_start = wallclock;
run_walt_irq_work(old_window_start, rq);
}
该函数主要完成以下关键功能:
时间窗口管理:维护 WALT 算法的时间窗口机制
任务负载跟踪:更新任务的 CPU 需求(demand)和预测需求(pred_demand)
CPU 状态更新:记录 CPU 的繁忙时间和周期计数
统计信息维护:为调度决策提供数据基础
代码逻辑
WALT算法以任务为主,当任务被唤醒、任务开始执行、任务停止执行、任务退出、窗口滚动、频率变化、任务迁徙、经过一个调度tick、在中断结束时才会调用update_task_ravg()。
其中,窗口时WALT算法中的一个特殊设定,详细解释在update_task_demand() 与 update_cpu_busy_time() 中,感兴趣可以看一下。
1.判断是否进入WALT算法
在进入WALT算法后首先会判断当前任务所在的运行队列(runqueue)是否进行初始化,以及是否禁用CPU的窗口统计:if(!rq->window_start || sched_disable_window_stats...)
。如果没有初始化,就不会记录窗口的开始时间,任务负载就无法进行计算。有几点需要注意:
- 该处是指rq,而非cfs->rq 或 rt->rq,即该处不区分实时任务或普通任务。
- 任务/CPU窗口(sched_ravg_window)是自定义的,不同版本代码或不同设备中设置的窗口大小是不一样的,调整的位置也不尽相同。
然后回判断窗口开始时间是否更新:if(...p->ravg.mark_start == wallclock)
。如果运行队列没有初始化,或禁用了CPU的窗口统计,或窗口开始时间没有更新,就会直接结束WALT算法。
2.获取WALT算法中上一个窗口的开始时间
然后通过函数update_window_start()
获取上一个窗口的开始时间,存在变量old_window_start
中。
3.如果任务刚初始化结束
如果任务刚初始化结束:if(!p->ravg.mark_start)
,还没有标记过任务的开始时间,就先通过函数 update_task_cpu_cycles()
更新一下该任务的 cycles 值(p->cpu_cycles
),然后进入 done
。
4.更新任务及CPU的cycles
和 update_task_cpu_cycles()
相似,但比其多更新了 CPU 的 cycles 值(rq->cc.cycles
)。该函数更新的值会在 scale_exec_time() 中被用到。
5.更新任务及 CPU 的demand 及 perd_demand
在任务满足条件后,在不同情况下根据任务的开始时间、窗口的开始时间以及当前时间来计算任务在当前及之前M哥窗口中的运行时间。在窗口结束时将运行时间进行归一化,并统计进任务的历史窗口中(sum_history[RAVG_HIST_SIZE])。WALT算法根据历史窗口中的值计算任务的demand,根据桶算法计算任务的perd_demand,并将demand 与 pred_demand 统计进任务所在的CPU 的 rq(runqueue)中。
注意:以上的demand 和 pred_demand都是预测值。
6.更新 CPU 的busy time
在任务满足条件后,在不同情况下根据任务的开始时间、窗口的开始时间以及当前时间来计算任务在当前及上一个窗口中的运行时间,将不同窗口内的运行时间进行归一化,并根据任务的状态统计进任务的 curr_window
和 prev_window
中,以及任务所在rq的 curr_runnable_sum
和 prev_runnable_sum
中。
在窗口翻滚的时候更新任务的 window 值 以及 rq 的 runnable_sum的值。
注意:以上的 window 和 runnable_sum 都是真实值。
7.更新任务的 pred_demand
如果符合条件的任务在当前窗口中预测出来的 pred_demand 值小于 curr_window,则再次使用桶算法计算 pred_demand。
update_task_pred_demand() 代码详解。
8.如果任务正在退出
#define EXITING_TASK_MARKER 0xdeaddead
static inline int exiting_task(struct task_struct *p)
{
return (p->ravg.sum_history[0] == EXITING_TASK_MARKER);
}
当任务最近一个窗口的值为 0xdeaddead 时,意味着任务正在退出,进入 done
。
done 结束部分
- 更新下一个任务的开始时间:
p->ravg.mark_start = wallclock
。 - 执行
run_walt_irq_work()
,判断是否需要计算频率。
WALT 频率计算详解
WALT (Window-Assisted Load Tracking) 是Linux内核中用于任务调度和CPU频率调节的一种负载跟踪机制,特别是在EAS (Energy Aware Scheduling) 框架中使用。下面我将详细解释WALT的频率计算原理。
WALT 基本概念
WALT通过时间窗口的方式来跟踪CPU和任务的负载,主要特点包括:
窗口化统计:将时间划分为固定长度的窗口(通常为20ms),在每个窗口内统计负载
历史记录:维护多个窗口的历史数据
即时响应:相比传统的PELT(Per-Entity Load Tracking)能更快响应负载变化
频率计算核心原理
WALT的频率计算主要基于以下几个关键指标:
1.需求计算
对于每个任务和CPU,WALT计算两个关键指标:
-
demand:任务在窗口期间实际使用的CPU时间
-
pred_demand:预测的任务需求(基于历史需求)
计算公式:
demand = (实际运行时间 / 窗口时间) * CPU最大频率
pred_demand = 根据历史demand预测的未来需求
2.CPU负载聚合
对于每个CPU,聚合所有运行任务的demand:
cpu_demand = Σ(task_demand)
cpu_pred_demand = Σ(task_pred_demand)
3.调频选择
调度器根据一下因素选择目标频率:
target_freq = min(max(cpu_demand, cpu_pred_demand), max_freq)
其中:
-
max_freq
是CPU支持的最大频率 -
选择
demand
和pred_demand
中的较大值是为了防止突然的负载增加导致性能下降
4.考虑CPU容量
频率选择还会考虑CPU的容量差异(在大.LITTLE架构中):
scaled_demand = demand * (max_freq / max_possible_freq)
计算流程详细步骤
窗口初始化:每个20ms窗口开始时重置统计
运行时间统计:记录任务在窗口内的实际运行时间
窗口结束处理:
计算当前窗口的demand
更新历史demand数据
计算预测的pred_demand
频率请求:
聚合所有运行任务的demand
考虑CPU拓扑和容量
向CPUFreq governor提出频率请求
Governor决策:最终由schedutil等governor决定实际设置的频率
与传统PELT的区别
响应速度:WALT对突发负载响应更快,PELT有较长的衰减周期
计算方式:WALT使用离散窗口,PELT使用连续衰减
频率请求:WALT直接基于窗口需求,PELT基于长期平均负载
实际实现考虑
在Linux内核中,WALT的频率计算还涉及:
boost机制:某些场景下会临时提升频率请求
调频延迟:考虑硬件调频的实际延迟
能效考虑:在性能需求满足的前提下尽量选择能效比高的频率
这种计算方式使得系统能够快速响应负载变化,同时兼顾能效和性能。
函数调用时机
<kernel/sched/core.c>
try_to_wake_up()
try_to_wake_up_local()
scheduler_tick()
__schedule()
sched_exit()
<kernel/sched/walt.c>
sched_account_irqtime()
fixup_busy_time()
cpufreq_notifier_trans()
transfer_busy_time()
walt_irq_work()