WALT(Window Assisted Load Tracking窗口辅助负载跟踪算法)的核心算法思想是:以时间窗口长度window为单位,跟踪CPU使用情况,用前面N个窗口的CPU使用情况预测当前窗口的CPU需求。窗口默认长度是20ms,walt_ravg_window = (20000000 / TICK_NSEC) * TICK_NSEC,进程负载计算默认是5个历史窗口(#define RAVG_HIST_SIZE_MAX 5),CPU负载计算只用一个历史窗口。要理解WALT算法需要理解几个概念:WALT时间、cpu_scale、freq_scale。

WALT1.png

WALT时间是什么呢,是进程占用CPU时间吗?想象一下,有两个进程,在一个时间窗口内,A进程在2.6G频率上运行了5ms,B进程在小核500M频率上运行了5ms。如果仅考虑CPU占用时间,那么它们的负载是相同的,这显然与实际情况不符。所以WALT时间,不仅要考虑CPU占用时间,还要考虑所在CPU的运算能力、运行频率。函数scale_exec_time()用于计算WALT时间,参数delta是CPU运行时间,rq是进程所在运行队列(与CPU对应),SCHED_CAPACITY_SHIFT是10。

static u64 scale_exec_time(u64 delta, struct rq *rq)
{
    unsigned long capcurr = capacity_curr_of(cpu_of(rq));
 
    return (delta * capcurr) >> SCHED_CAPACITY_SHIFT;
}
 
unsigned long capacity_curr_of(int cpu)
{
    return cpu_rq(cpu)->cpu_capacity_orig *
           arch_scale_freq_capacity(NULL, cpu)
           >> SCHED_CAPACITY_SHIFT;
} 

capacity_curr_of()中cpu_capacity_orig的值最终来源于per_cpu变量cpu_scale。

#define arch_scale_cpu_capacity scale_cpu_capacity
static void update_cpu_capacity(struct sched_domain *sd, int cpu)
{
    unsigned long capacity = arch_scale_cpu_capacity(sd, cpu);
    struct sched_group *sdg = sd->groups;
    struct max_cpu_capacity *mcc;
    unsigned long max_capacity;
    int max_cap_cpu;
    unsigned long flags;
 
    cpu_rq(cpu)->cpu_capacity_orig = capacity;
    ...............................................................
}
unsigned long scale_cpu_capacity(struct sched_domain *sd, int cpu)
{
    return per_cpu(cpu_scale, cpu);
}

capacity_curr_of()中arch_scale_freq_capacity()最终是获取的per_cpu变量freq_scale。

#define arch_scale_freq_capacity cpufreq_scale_freq_capacity
unsigned long cpufreq_scale_freq_capacity(struct sched_domain *sd, int cpu)
{
    return per_cpu(freq_scale, cpu);
}

上面计算WALT时间的代码,可以简化为如下的运算公式:

WALT2.png

cpu_scale是当前CPU运算能力尺度化。市面上有各种各样的CPU,它们的运算能力各不相同,同一系列的CPU也会迭代升级运算能力,如果以CPU实际运算能力为参数,很难做到算法的统一。内核用CPU运算能力尺度化来解决该问题,定义系统中最高运算能力核的cpu_scale为1024,其它核的cpu_scale为该(CPU运算能力/最大核运算能力)*1024。CPU各个核的cpu_scale都由SOC厂商给出,MTK平台可以cat各cpu目录下cpu_capacity节点查看cpu_scale。

WALT3.png

WALT4.png

freq_scale是某CPU当前频率运算能力的瓷都化,系统定义当前核最高频率运行时的freq_scale为1024,其它频率的freq_scale为当前频率运算能力/最高频率运算能力*1024。为什么用频率的运算能力比,而不是频率比呢?是因为有些厂商的CPU是CONFIG_NONLINER_FREQ_CTL类型的,它的运算能力与频率不是正比关系。例如MTK的某些SOC就是这样的芯片,它的运算能力与频率不成正比,只能查表来看某频点运算能力。

WALT5.png

综上,我们可以把WALT时间的运算公式做如下表示:
WALT6.png

从上面的公式可以看出,进程只有在最大核的最高频点上运行时,其CPU占用时间才会等于WALT时间,其它情况WALT时间都是CPU占用时间按一定比例缩短的结果。

1.进程负载计算

进程的task_struct中有一个ravg类型的变量用于保存WALT相关的信息。其中mark_start是上一次统计的时间,sum指进程在当前窗口已经运行的WALT时间,数组sum_history[RAVG_HIST_SIZE_MAX]保存进程在前5个历史窗口期内的WALT时间,demand是由sum_history[]历史数据推算出的值。

struct ravg {
    u64 mark_start;
    u32 sum, demand;
    u32 sum_history[RAVG_HIST_SIZE_MAX];
    u32 curr_window, prev_window;
    u16 active_windows;
};

在进程切换等情况下,会统计WALT时间,有三种情况。情况一,如果当前时间(wallclock)与上次统计时间(mark_start)在一个时间窗口内,只需要将wallclock - mark_start转换为WALT时间后累加到ravg.sum即可。情况二,如果当前时间(wallclock)与上次统计时间(mark_start)跨了一个窗口,首先计算mark_start到当前窗口起始位置这部分WALT时间,并累加到ravg.sum后调用update_history(),将ravg.sum存入历史窗口。然后计算当前窗口起始时间到现在(wallclock)的这部分WALT时间,并赋值到ravg.sum。情况三,如果当前时间(wallclock)与上次统计时间(mark_start)跨了多个窗口,首先计算mark_start到它下一个窗口起始位置这部分WALT时间,并累加到ravg.sum后调用update_history(),将ravg.sum存入历史窗口。然后计算中间跨过窗口的WALT时间并更新到历史窗口中,最后计算当前窗口起始时间到现在(wallclock)的这部分WALT时间,并赋值到ravg.sum。

WALT7.png

//进程切换时更新负载
static void __sched notrace __schedule(bool preempt)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct pin_cookie cookie;
    struct rq *rq;
    int cpu;
    u64 wallclock;
    ................................................................
    next = pick_next_task(rq, prev, cookie);
    wallclock = walt_ktime_clock();
    walt_update_task_ravg(prev, rq, PUT_PREV_TASK, wallclock, 0);
    walt_update_task_ravg(next, rq, PICK_NEXT_TASK, wallclock, 0);
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
    ................................................................
}
 
void walt_update_task_ravg(struct task_struct *p, struct rq *rq,
         int event, u64 wallclock, u64 irqtime)
{
    if (walt_disabled || !rq->window_start)
        return;
 
    lockdep_assert_held(&rq->lock);
    //更新窗口起始位置
    update_window_start(rq, wallclock);
 
    if (!p->ravg.mark_start)
        goto done;
    //更新进程负载
    update_task_demand(p, rq, event, wallclock);
    update_cpu_busy_time(p, rq, event, wallclock, irqtime);
 
done:
    trace_walt_update_task_ravg(p, rq, event, wallclock, irqtime);
 
    p->ravg.mark_start = wallclock;
}
 
//更新窗口起始位置
static void update_window_start(struct rq *rq, u64 wallclock)
{
    s64 delta;
    int nr_windows;
 
    delta = wallclock - rq->window_start;
    ..............................................................
    if (delta < walt_ravg_window)
        return;
 
    nr_windows = div64_u64(delta, walt_ravg_window);
    rq->window_start += (u64)nr_windows * (u64)walt_ravg_window;
 
    rq->cum_window_demand = rq->cumulative_runnable_avg;
}
 
static void update_task_demand(struct task_struct *p, struct rq *rq,
         int event, u64 wallclock)
{
    u64 mark_start = p->ravg.mark_start;
    u64 delta, window_start = rq->window_start;
    int new_window, nr_full_windows;
    u32 window_size = walt_ravg_window;
 
    new_window = mark_start < window_start;
    ................................................................
    //情况一
    if (!new_window) {
        /* The simple case - busy time contained within the existing
         * window. */
        add_to_task_demand(rq, p, wallclock - mark_start);
        return;
    }
    //情况二、三代码相同,只是情况二nr_full_windows为0
    delta = window_start - mark_start;
    nr_full_windows = div64_u64(delta, window_size);
    window_start -= (u64)nr_full_windows * (u64)window_size;
    //计算mark_start到它下一个窗口起始位置之间的WALT时间
    add_to_task_demand(rq, p, window_start - mark_start);
 
    /* Push new sample(s) into task's demand history */
    update_history(rq, p, p->ravg.sum, 1, event);
    //计算中间跨越窗口的WALT时间
    if (nr_full_windows)
        update_history(rq, p, scale_exec_time(window_size, rq),
                   nr_full_windows, event);
 
    /* Roll window_start back to current to process any remainder
     * in current window. */
    window_start += (u64)nr_full_windows * (u64)window_size;
 
    /* 计算当前窗口起始时间到现在之间的WALT时间 */
    mark_start = window_start;
    add_to_task_demand(rq, p, wallclock - mark_start);
}

demand值是在update_history()中更新的,有四种策略可选:WINDOW_STATS_RECENT,用本次更新进来的值;WINDOW_STATS_MAX,用所有历史记录中的最大值;WINDOW_STATS_AVG,用历史记录中的平均值;WINDOW_STATS_MAX_RECENT_AVG,本次更新进来的值与历史平均值中较大的那个。

#define WINDOW_STATS_RECENT        0
#define WINDOW_STATS_MAX        1
#define WINDOW_STATS_MAX_RECENT_AVG    2
#define WINDOW_STATS_AVG        3
#define WINDOW_STATS_INVALID_POLICY    4
 
static void update_history(struct rq *rq, struct task_struct *p,
             u32 runtime, int samples, int event)
{
    u32 *hist = &p->ravg.sum_history[0];
    int ridx, widx;
    u32 max = 0, avg, demand;
    u64 sum = 0;
 
    /* Ignore windows where task had no activity */
    if (!runtime || is_idle_task(p) || exiting_task(p) || !samples)
            goto done;
 
    /* Push new 'runtime' value onto stack */
    widx = walt_ravg_hist_size - 1;
    ridx = widx - samples;
    for (; ridx >= 0; --widx, --ridx) {
        hist[widx] = hist[ridx];
        sum += hist[widx];
        if (hist[widx] > max)
            max = hist[widx];
    }
 
    for (widx = 0; widx < samples && widx < walt_ravg_hist_size; widx++) {
        hist[widx] = runtime;
        sum += hist[widx];
        if (hist[widx] > max)
            max = hist[widx];
    }
 
    p->ravg.sum = 0;
 
    if (walt_window_stats_policy == WINDOW_STATS_RECENT) {
        demand = runtime;
    } else if (walt_window_stats_policy == WINDOW_STATS_MAX) {
        demand = max;
    } else {
        avg = div64_u64(sum, walt_ravg_hist_size);
        if (walt_window_stats_policy == WINDOW_STATS_AVG)
            demand = avg;
        else
            demand = max(avg, runtime);
    }
    ....................................................................
    if (!task_has_dl_policy(p) || !p->dl.dl_throttled) {
        if (task_on_rq_queued(p))
            fixup_cumulative_runnable_avg(rq, p, demand);
        else if (rq->curr == p)
            fixup_cum_window_demand(rq, demand);
    }
 
    p->ravg.demand = demand;
    ....................................................................
}

进程负载的计算公式如下,可以看出1024就是满负载时的值,进程满负载必须满足:在整个时间窗口内都处于运行状态,并且所在核是大核,运行频率是大核最高频率。

WALT8.png

static inline unsigned long task_util(struct task_struct *p)
{
#ifdef CONFIG_SCHED_WALT
    if (!walt_disabled && (sysctl_sched_use_walt_task_util ||
                p->prio < sched_use_walt_nice)) {
        unsigned long demand = p->ravg.demand;
        return (demand << SCHED_CAPACITY_SHIFT) / walt_ravg_window;
    }
#endif
    return p->se.avg.util_avg;
}

2.CPU负载计算

CPU负载计算是在update_cpu_busy_time()中完成的,计算方法与进程负载类似。不同的是,CPU负载计算只用了一个历史窗口,就是运行队列中的prev_runnable_sum。

static inline unsigned long cpu_util_freq(int cpu)
{
    unsigned long util = cpu_rq(cpu)->cfs.avg.util_avg;
    unsigned long capacity = capacity_orig_of(cpu);
 
#ifdef CONFIG_SCHED_WALT
    if (!walt_disabled && sysctl_sched_use_walt_cpu_util)
        util = div64_u64(cpu_rq(cpu)->prev_runnable_sum,
                walt_ravg_window >> SCHED_CAPACITY_SHIFT);
#endif
    return (util >= capacity) ? capacity : util;
}