调度器5—CFS负载计算-1_PELT_不考虑CFS组调度和带宽控制-legency-不再补充

1. 负载结构描述

(1) 每个调度实体都有一个负载结构,用来跟踪调度实体对系统的负载贡献,定义如下:

struct sched_entity { 
    struct load_weight    load;  
    #ifdef CONFIG_SMP
        struct sched_avg    avg;
    #endif
};

/*
 * load_avg/util_avg 累积了一个无限几何系列(参见 kernel/sched/fair.c 中的 __update_load_avg())。
 *
 * load_avg 的定义:load_avg = runnable% * scale_load_down(load)
 *
 * 解释:runnable% 是 sched_entity 的 runnable 时间的占比。对于 cfs_rq 的,它是所有的
 * runnable 和 blocked sched_entitiy 的 load_avg 总和。
 *
 * util_avg 的定义:util_avg = running% * SCHED_CAPACITY_SCALE
 *
 * 解释:running% 是一个 sched_entity 在 cpu 上 running 的时间占比。对于 cfs_rq 的,
 * 它是所有的 runnable 和 blocked sched_entitiy 的 util_avg 的总和。
 *
 * load_avg 和 util_avg 不直接影响频率缩放和 CPU 算力缩放。 缩放是通过用于计算这些信号的 rq_clock_pelt 完成的(请参阅 update_rq_clock_pelt())
 *
 * 注意,上述比率(runnable% 和 running%)本身在 [0, 1] 的范围内。 因此,为了进行定点算术,我们将它们按需要缩放到尽可能大的范围。 
 * 这由 util_avg 的 SCHED_CAPACITY_SCALE 反映。
 *
 * [溢出问题]
 * 64 位 load_sum 可以有 4353082796 (=2^64/47742/88761) 个具有最高负载 (=88761) 的实体,
 * 一直在一个 cfs_rq 上运行,并且不会溢出,因为数量已经达到 PID_MAX_LIMIT。也就是说64bit的溢出问题不用担心(88761为prio=100的cfs的weight,
 * sched_prio_to_weight[0],47742是按PELT算法计算出的一直满跑计算出来的结果,#define LOAD_AVG_MAX 47742)
 * 对于所有其他情况(包括 32 位内核),struct load_weight 的权重将在我们之前先溢出,因为: Max(load_avg) <= Max(load.weight)
 * 因此考虑溢出问题是 load_weight 的责任。
* 意思是64位长度的 load_sum 值可以记录 2^64/4772/88761 个最大权重(优先级最高)且一直满跑的任务。
*/ struct sched_avg { u64 last_update_time; /* 上次负载更新时间,单位ns */ u64 load_sum; /* 负载贡献,累计runable和block衰减负载 */ u64 runnable_load_sum;/* runable状态负载贡献 */ u32 util_sum; /* running状态负载贡献,累计running衰减时间总和,是又移过10的 */ u32 period_contrib; /* 负载计算时,不足一个周期的部分 */ unsigned long load_avg; /* 平均负载,(load_sum*load->weight)/最大衰减值 */ unsigned long runnable_load_avg; /* runable 状态平均负载 */ unsigned long util_avg; /* running状态的比值,util_avg 的定义:util_avg = running% * SCHED_CAPACITY_SCALE */ struct util_est util_est; /* task唤醒的时候预估的负载 */ } /* * struct util_est - 估计 FAIR 任务的利用率(utilization) * @enqueued:task/cpu 的瞬时利用率估计 * @ewma:任务的指数加权移动平均 (EWMA) 利用率 * * 支持数据结构以跟踪 FAIR 任务利用率的指数加权移动平均值 (EWMA)。每次任务完成唤醒时,都会将新样本添加到移动平均值中。 * 选择样本的权重以使 EWMA 对任务工作负载的瞬态变化相对不敏感。 * * enqueued 属性对于 tasks 和 cpus 的含义略有不同: * - task:上次任务出队时任务的 util_avg * - cfs_rq:该 CPU 上每个 RUNNABLE 任务的 util_est.enqueued 总和。因此,任务(非cfs_rq)的 util_est.enqueued 表示该任务当前排队的 CPU 估计利用率的贡献。 * * 仅对于我们跟踪过去瞬时估计利用率的移动平均值的任务。这允许吸收其他周期性任务的利用率的零星下降。 */ struct util_est { unsigned int enqueued; unsigned int ewma; #define UTIL_EST_WEIGHT_SHIFT 2 } __attribute__((__aligned__(sizeof(u64)))); struct cfs_rq { ... /*挂入改cfs_rq的调度实体的负载权重之和*/ struct load_weight load; /*挂入改cfs_rq的调度实体的runnable weight之和*/ unsigned long runnable_weight; /* CFS load tracking PELT算法跟踪的该cfs_rq的平均负载和利用率 */ struct sched_avg avg; ... /* * 当一个任务退出或唤醒后迁移到其它cpu的时候,那么原本的cpu上的cfs_rq *上需要移除该任务带来的负载。由于持rq锁的问题,所以先把移除的负载记录 * 在这个成员中,适当的时机再更新之。 */ struct { raw_spinlock_t lock ____cacheline_aligned; /*remove_entity_load_avg中加1*/ int nr; /*remove_entity_load_avg中加上en的这个域*/ unsigned long load_avg; /*remove_entity_load_avg中加上en的这个域*/ unsigned long util_avg; /*remove_entity_load_avg中加上en的这个域*/ unsigned long runnable_sum; } removed; ... };

(2) 补充

调度实体 sched_entity 和 cfs_rq 都内嵌一个 shed_avg 结构。

最大衰减累加时间:进程在CPU上运行无限长时,根据 PELT 算法计算出的衰减值。当进程无限运行后,load_avg 总是无限接近进程权重值(load.weight)

对调度实体来说:load_sum=runnable_load_sum, load_avg=runnable_load_avg。对于CFS调度队列来说:load_sum = 整个队列负载 * 整个队列权重。

 

2. 负载更新函数

static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) //fair.c
{
    u64 now = cfs_rq_clock_pelt(cfs_rq);
    int decayed; //衰变的

    /*
     * Track task load average for carrying it to new CPU after migrated, and
     * track group sched_entity load average for task_h_load calc in migration
     */
    if (se->avg.last_update_time && !(flags & SKIP_AGE_LOAD)) { //应该是首次不进入
        /* 更新调度实体负载 */
        __update_load_avg_se(now, cfs_rq, se);
    }

    /*更新CFS队列负载*/
    decayed  = update_cfs_rq_load_avg(now, cfs_rq);
    decayed |= propagate_entity_load_avg(se); //若不使能CONFIG_FAIR_GROUP_SCHED函数只直接返回0

    if (!se->avg.last_update_time && (flags & DO_ATTACH)) {
        /*
         * DO_ATTACH 表示是在 enqueue_entity() 的执行路径中。
         * !last_update_time means 表示被迁移的
         *
         * 我们正在往一个新的CPU上enqueue task
         */
        attach_entity_load_avg(cfs_rq, se, SCHED_CPUFREQ_MIGRATION);
        update_tg_load_avg(cfs_rq, 0); //若不使能CONFIG_FAIR_GROUP_SCHED函数只直接返回0
    } else if (decayed) {
        cfs_rq_util_change(cfs_rq, 0);

        if (flags & UPDATE_TG)
            update_tg_load_avg(cfs_rq, 0);
    }
}

只要使能了 CONFIG_SMP 使用的就是这个函数,里面同时更新了调度实体和cfs_rq的负载,无论是否使用 WALT 负载跟踪算法。先从简单入,先不看 CONFIG_FAIR_GROUP_SCHED 和 CONFIG_CFS_BANDWIDTH 使能时使用的代码。

(1) update_load_avg 的主要执行过程如下:
a. 更新本层级sched entity的load avg(__update_load_avg_se)
b. 更新该se挂入的cfs rq的load avg(update_cfs_rq_load_avg)
c. 如果是group se,并且cfs--se层级结构有了调度实体的变化,那么需要处理向上传播的负载。在tick场景中,不需要这个传播过程。
d. 更新该层级task group的负载(update_tg_load_avg)。之所以计算task group的load avg,这个值后续会参与计算group se的load weight。


(2) 负载更新时机

        enqueue_entity //传参(cfs_rq, se, UPDATE_TG | DO_ATTACH)
        dequeue_entity //传参(cfs_rq, se, UPDATE_TG)
        set_next_entity //传参(cfs_rq, se, UPDATE_TG)
        put_prev_entity //传参(cfs_rq, prev, 0)
        entity_tick //传参(cfs_rq, curr, UPDATE_TG)
        enqueue_task_fair //传参(cfs_rq, se, UPDATE_TG) 会和enqueue_entity中的更新重复吗?
        dequeue_task_fair //传参(cfs_rq, se, UPDATE_TG)
update_nohz_stats
_nohz_idle_balance //if (idle != CPU_NEWLY_IDLE)时调用
newidle_balance
run_rebalance_domains
    update_blocked_averages
        __update_blocked_fair //传参(cfs_rq_of(se), se, 0)
    detach_entity_cfs_rq
    attach_entity_cfs_rq
        propagate_entity_cfs_rq //传参(cfs_rq, se, UPDATE_TG)
fair_sched_class.migrate_task_rq
    migrate_task_rq_fair //if (p->on_rq == TASK_ON_RQ_MIGRATING)成立调用
switched_from_fair //.switched_from回调,rt_mutex_setprio/__sched_setscheduler-->check_class_changed-->switched_from
task_move_group_fair
    detach_task_cfs_rq
        detach_entity_cfs_rq //传参(cfs_rq, se, 0)
        attach_entity_cfs_rq //传参(cfs_rq, se, sched_feat(ATTACH_AGE_LOAD) ? 0 : SKIP_AGE_LOAD)
    proc_sched_autogroup_set_nice
cpu_legacy_files.write_u64 //回调函数,对应cpu cgroup的"shares"文件
    cpu_shares_write_u64 //使能CONFIG_FAIR_GROUP_SCHED才有
cpu_files.write_u64 //回调函数,对应cpu cgroup的"weight"文件
    cpu_weight_write_u64
cpu_files.write_s64 //回调函数,对应cpu cgroup的"weight.nice"文件
    cpu_weight_nice_write_s64
        sched_group_set_shares //传参(cfs_rq_of(se), se, UPDATE_TG)
            update_load_avg

主要是任务入队列、出队列对应的时机调用,update_curr()中并没有调用。

 

4. 调度实体负载更新

int __update_load_avg_se(u64 now, struct cfs_rq *cfs_rq, struct sched_entity *se) //pelt.c
{
    /*
     * 返回真,表示经历了一个完整周期(1024us)。从传参可以看出,对 se->sa->load_sum 和
     * se->sa->runnable_load_sum 是否衰减更新取决于其是否在cfs_rq队列上,对 se->sa->util_sum
     * 是否衰减更新取决于其是否为正在运行的任务。
     */
    if (___update_load_sum(now, &se->avg, !!se->on_rq, !!se->on_rq, cfs_rq->curr == se)) {
        /*传参:(&se->avg, max(2, se->load.weight>>10), max(2, se->runnable_weight>>10))*/
        ___update_load_avg(&se->avg, se_weight(se), se_runnable(se));
        /*更新se->avg.util_est.enqueued*/
        cfs_se_util_change(&se->avg);
        trace_pelt_se_tp(se);
        return 1;
    }

    return 0;
}

(1) ___update_load_sum 函数

/*
 * 我们可以将可运行(runnable)平均值的历史贡献表示为几何级数的系数。 为此,我们将可运行(runnable)历史细分为大约 1ms (1024us) 的段; 
 * 标记发生在 N 毫秒前的为 p_N 的段,p_0 对应于当前周期,例如
 * [<- 1024us ->|<- 1024us ->|<- 1024us ->| ...
 *      p0            p1            p2
 *     (now)        (~1ms ago)    (~2ms ago)
 *
 * 让 u_i 表示实体可运行的 p_i 分数。
 *
 * 然后我们指定分数 u_i 作为我们的系数,产生以下历史负载表示:
 *   u_0 + u_1*y + u_2*y^2 + u_3*y^3 + ...
 * 我们根据合理的调度周期选择 y,固定:
 *   y^32 = 0.5
 * 这意味着大约 32 毫秒前 (u_32) 的负载对负载的贡献将是当前(u_0)的负载对负载的贡献的一半。
 * 当一个周期“滚动”并且我们有新的 u_0 时,将先前的总和再次乘以 y 就可以了:
 *   load_avg = u_0 + y*(u_0 + u_1*y + u_2*y^2 + ... )
 *               = u_0 + u_1*y + u_2*y^2 + ... [re-labeling u_i --> u_{i+1}]
 */
/*
 * update_load_avg调用传参:(now, &se->avg, !!se->on_rq, !!se->on_rq, cfs_rq->curr == se)
 * 根据运行时间 delta 值,调用 accumulate_sum() 先将原负载进行衰减,然后计算出新负载,然后
 * 再累加到衰减后的负载上。delta是经历了一个完整周期(就是加上上一次计算剩余的不足一个周期的
 * sa->period_contrib部分后大于1024us)就返回1,否则返回0.
 */
static __always_inline int ___update_load_sum(u64 now, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running) //pelt.c
{
    u64 delta;

    delta = now - sa->last_update_time;
    /*这只有时间反向增长的时候才可能发生,在翻滚后sched clock init时会不幸的发生*/
    if ((s64)delta < 0) {
        sa->last_update_time = now;
        return 0;
    }

    /*为了快速计数,以us为单位进行对比,这里大约地将1024ns转换为1us,小于1us直接退出 */
    delta >>= 10;
    if (!delta)
        return 0;

    sa->last_update_time += delta << 10; //更新last_update_time,没有更新回delta

    /*
     * running 是 runnable (weight) 的一个子集,因此如果 runnable 被清理了,则无法设置 running。 
     * 但是在某些极端情况下,当前 se 已经 dequeue 了,但 cfs_rq->curr 仍然指向它。 这意味着权重
     * 将为 0,但不会让此 sched_entity 运行,如果 cfs_rq 空闲,也会不为 cfs_rq 运行。 例如,这
     * 发生在调用 update_blocked_averages() 的 idle_balance() 期间。
     */
    if (!load)
        runnable = running = 0; //若第一个参数传0,第二第三个参数也赋值为0

    /*
     * 现在我们知道我们通过了测量单位边界的判断。 *_avg 分两步累积:
     * 第 1 步:自 last_update_time 累积 *_sum。 如果我们还没有跨越时期界限,那就结束吧。
     */
    if (!accumulate_sum(delta, sa, load, runnable, running))
        return 0;

    return 1;
}


/*
 * 累加三个独立部分的总和; d1 上一个(不完整)周期的剩余时间,d2 整个周期的跨度,d3 是(不完整)当前周期的剩余部分。
 *
 *           d1          d2           d3
 *           ^           ^            ^
 *           |           |            |
 *         |<->|<----------------->|<--->|
 * ... |---x---|------| ... |------|-----x (now)
 *
 *                           p-1
 * u' = (u + d1) y^p + 1024 \Sum y^n + d3 y^0
 *                           n=1
 *
 *    = u y^p +                    (Step 1)
 *
 *                     p-1
 *      d1 y^p + 1024 \Sum y^n + d3 y^0        (Step 2)
 *                     n=1
 */
/*此函数作用:先将原负载进行衰减,然后计算出新负载,然后再累加到衰减后的负载上*/
static __always_inline u32 accumulate_sum(u64 delta, struct sched_avg *sa, unsigned long load, unsigned long runnable, int running)
{
    u32 contrib = (u32)delta; /* p == 0 -> delta < 1024,此时delta应该对应上面的d1+d2+d3的和,此时的delta的单位是us,在传入之前时间做差后右移10了 */
    u64 periods;

    /* 这里加的是d1前面不足1个周期的部分(看来负载是按整周期计算的,不足一个周期的部分放在这里) */
    delta += sa->period_contrib;
    periods = delta / 1024; /* A period is 1024us (~1ms) 除以1024将us转化为ms */

    /* 第 1 步:如果我们跨越了周期边界,则衰减旧的 *_sum。*/
    if (periods) {
        /*这里只是将原来的负载衰减periods个周期*/
        sa->load_sum = decay_load(sa->load_sum, periods); //执行 sa->load_sum * y^periods 也就是对原负载进行衰减
        sa->runnable_load_sum = decay_load(sa->runnable_load_sum, periods); //执行 sa->runnable_load_sum * y^periods
        sa->util_sum = decay_load((u64)(sa->util_sum), periods); //执行 sa->util_sum * y^periods

        /* Step 2 */
        /* 不足一个周期(1024us)的部分,就是d3,上面dalta加上了sa->period_contrib后使d1部分也凑足一个周期了,
         * 所以delta %= 1024后是d3。1024 - sa->period_contrib 就是d1。 
         * __accumulate_pelt_segments的形参:(u64 periods, u32 d1, u32 d3)
         */
        delta %= 1024;
        contrib = __accumulate_pelt_segments(periods, 1024 - sa->period_contrib, delta);
    }
    /* 将本次运行的不足一个周期的部分赋值给sa->period_contrib,其单位是us */
    sa->period_contrib = delta;

    /* __update_load_avg_se()传参传下来的手势bool值,!!se->on_rq。这里可以看出这三者的计算区别 */
    if (load)
        sa->load_sum += load * contrib;
    if (runnable)
        sa->runnable_load_sum += runnable * contrib;
    if (running)
        sa->util_sum += contrib << SCHED_CAPACITY_SHIFT; //这里应该不应理解为将us又转换为ns,这些*_sum使用的contribute都是us为单位的,这里应该理解为scale_up好一点。

    return periods;
}

static u32 __accumulate_pelt_segments(u64 periods, u32 d1, u32 d3) //pelt.c
{
    u32 c1, c2, c3 = d3; /* y^0 == 1 */

    /*
     * c1 = d1 y^p
     */
    /* 执行d1*y^periods, 这里是对d1多衰减一个周期的,看上图可知,d1应该衰减periods-1个周期。*/
    c1 = decay_load((u64)d1, periods);

    /*
     *            p-1
     * c2 = 1024 \Sum y^n
     *            n=1
     *
     *              inf        inf
     *    = 1024 ( \Sum y^n - \Sum y^n - y^0 )
     *              n=0        n=p
     *
     * 1024 表示满窗(1024us),c2是近似值,n越大结果就越小了,可以忽略不计了。
     */
    c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024;

    return c1 + c2 + c3;
}

__update_load_avg_se 函数只在 update_load_avg 中调用。

(2) ___update_load_avg 函数

/*传参:(&se->avg, max(2, se->load.weight>>10), max(2, se->runnable_weight>>10))*/
static __always_inline void ___update_load_avg(struct sched_avg *sa, unsigned long load, unsigned long runnable) //pelt.c
{
    /*当前窗口负载(sa->period_contrib) + 所有历史负载(LOAD_AVG_MAX - 1024)最大值,可理解为此任务一会满跑的情况下PELT计算出来的时间衰减累积值*/
    u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib;

    /* Step 2: update *_avg. *_avg = *_sum / divider */
    sa->load_avg = div_u64(load * sa->load_sum, divider);
    sa->runnable_load_avg = div_u64(runnable * sa->runnable_load_sum, divider);
    WRITE_ONCE(sa->util_avg, sa->util_sum / divider); //在sa->util_sum的计算时已经体现乘以1024了,因此除以的结果是 running% * 1024
}

由上面函数可以看出,*_avg = 优先级对应的权重值  x  *_sum / divider。解释起来就是,权重*历史负载/历史负载最大值,也就是说一个任务若是一直运行(running + runnable),其load_avg就接近其权重值。这里说的这个权重值是优先级对应的sched_prio_to_weight[] 的值,而不是 se->load.weight,因为这个成员保存的是 scale_up(就是乘以1024) 后的权重值,需要除以1024才是。

(3) cfs_se_util_change 函数

/*
 * 当任务出队时,如果其 util_avg 没有至少更新一次,则不应更新其估计利用率。
 * 此标志用于将 util_avg 更新与 util_est 更新同步。
 * 我们将此信息映射到出队时保存的利用率的 LSB 位(即 util_est.dequeued)。
 */
#define UTIL_AVG_UNCHANGED 0x1

static inline void cfs_se_util_change(struct sched_avg *avg)
{
    unsigned int enqueued;

    if (!sched_feat(UTIL_EST)) //默认为true
        return;

    /* Avoid store if the flag has been already set */
    enqueued = avg->util_est.enqueued;
    if (!(enqueued & UTIL_AVG_UNCHANGED)) //使用最后一bit来保持同步
        return;

    /* Reset flag to report util_avg has been updated */
    enqueued &= ~UTIL_AVG_UNCHANGED;
    WRITE_ONCE(avg->util_est.enqueued, enqueued);
}

更新 avg->util_est.enqueued,其最后一bit用来做同步。

 

5. cfs_rq 负载更新

/**
 * update_cfs_rq_load_avg - 更新 cfs_rq 的负载(load)/利用率(util)平均值
 * @now:当前时间,根据 cfs_rq_clock_pelt()
 * @cfs_rq: 要更新的 cfs_rq 
 *
 * cfs_rq avg 是其所有实体(blocked 和 runnable)avg 的直接总和。直接的
 * 推论是必须附加所有(公平的)任务,请参阅 post_init_entity_util_avg()。
 * 例如,cfs_rq->avg 用于 task_h_load() 和 update_cfs_share()。
 * 如果负载衰减或我们移除了负载,则返回 true。
 * 由于这两个条件都表明 cfs_rq->avg.load 发生了变化,我们应该在此函数返回 true 时调用 update_tg_load_avg()。
 */
static inline int update_cfs_rq_load_avg(u64 now, struct cfs_rq *cfs_rq) //fair.c
{
    unsigned long removed_load = 0, removed_util = 0, removed_runnable_sum = 0;
    struct sched_avg *sa = &cfs_rq->avg;
    int decayed = 0;

    if (cfs_rq->removed.nr) {
        unsigned long r;
        u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib; //divider恒这个计算公式

        raw_spin_lock(&cfs_rq->removed.lock);
        swap(cfs_rq->removed.util_avg, removed_util); //数值交换,为啥是交换而不是直接赋值?
        swap(cfs_rq->removed.load_avg, removed_load);
        swap(cfs_rq->removed.runnable_sum, removed_runnable_sum);
        cfs_rq->removed.nr = 0;
        raw_spin_unlock(&cfs_rq->removed.lock);

        r = removed_load;
        sub_positive(&sa->load_avg, r); //if(arg1-arg2 > arg1) arg1=0; 说明arg2是个负数(溢出)
        sub_positive(&sa->load_sum, r * divider);

        r = removed_util;
        sub_positive(&sa->util_avg, r);
        sub_positive(&sa->util_sum, r * divider);

        add_tg_cfs_propagate(cfs_rq, -(long)removed_runnable_sum); //不使能CONFIG_FAIR_GROUP_SCHED就是空函数

        decayed = 1;
    }

    decayed |= __update_load_avg_cfs_rq(now, cfs_rq);

#ifndef CONFIG_64BIT
    smp_wmb();
    cfs_rq->load_last_update_time_copy = sa->last_update_time;
#endif

    /*delta满足一个周期或cfs_rq->removed.nr不为0就返回1*/
    return decayed;
}

int __update_load_avg_cfs_rq(u64 now, struct cfs_rq *cfs_rq)
{
    /*传参:(now, &cfs_rq->avg, max(2, cfs_rq->load.weight>>10), max(2, cfs_rq->runnable_weight>>10), cfs_rq->curr != NULL)*/
    if (___update_load_sum(now, &cfs_rq->avg, scale_load_down(cfs_rq->load.weight), scale_load_down(cfs_rq->runnable_weight), cfs_rq->curr != NULL)) {
        /*由 *_sum 计算 *_avg 取公式:*_avg  = *_sum / divider*/
        ___update_load_avg(&cfs_rq->avg, 1, 1);
        trace_pelt_cfs_tp(cfs_rq);
        return 1;
    }

    return 0;
}

cfs_rq 的负载更新函数和 se 的调用的是同一个,只不过传参不同,se 的传参为 bool 值。cfs_rq 传的是数值,最终在计算负载的时候是累加此值乘以这段时间的负载贡献的。不太明白 #####

(1) update_cfs_rq_load_avg 的调用路径

//调用路径上面梳理了
update_load_avg
    update_cfs_rq_load_avg

    find_busiest_group
        update_sd_lb_stats
            update_sg_lb_stats
init_sched_fair_class //open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); 在软中断上下文执行
    run_rebalance_domains
        nohz_idle_balance
balance_fair
pick_next_task_fair //cfs_rq上没有任务可供pick的时候
    newidle_balance
        nohz_newidle_balance
            _nohz_idle_balance
                update_nohz_stats
                _nohz_idle_balance //if (idle != CPU_NEWLY_IDLE)才调用
                newidle_balance
                run_rebalance_domains
                    update_blocked_averages
                        __update_blocked_fair //CPU进入idle状态,那么意味着该CPU上的 load avg的负载都是blocked load,需要不间断进行衰减。
                            update_cfs_rq_load_avg

update_cfs_rq_load_avg 除了在 update_load_avg 函数中调用外,主要是在负载均衡路径中直接调用这个函数。

 

6. sched_pelt 中成员的计算

sched_pelt.h 中的内容来自 Documentation/scheduler/sched-pelt.c 计算得到的

kernel/msm-5.4/Documentation/scheduler$ gcc sched-pelt.c -o pp -lm 
kernel/msm-5.4/Documentation/scheduler$ ./pp
/* Generated by Documentation/scheduler/sched-pelt; do not modify. */

static const u32 runnable_avg_yN_inv[] __maybe_unused = {
        0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6, 
        0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85, 
        0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581, 
        0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9, 
        0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80, 
        0x85aac367, 0x82cd8698, 
};

#define LOAD_AVG_PERIOD 32
#define LOAD_AVG_MAX 47742

(1) runnable_avg_yN_inv 数组

runnable_avg_yN_inv[i] 的值为( (1UL<<32)-1) * y^i,其中 y = pow(0.5, 1/(double)LOAD_AVG_PERIOD),就是0.5开32次方就是y,y的32次方等于0.5,对应负载经历32个周期后(一个周期1024us~=1ms) 负载衰减到原来的1/2。runnable_avg_yN_inv[] 中一共有 LOAD_AVG_PERIOD 个元素。

(2) LOAD_AVG_PERIOD

y 的 LOAD_AVG_PERIOD 次方等于1/2,1/2 开 LOAD_AVG_PERIOD 次方等于y。

(3) LOAD_AVG_MAX

按照 PELT 负载计算算法能得出的最大负载的最大值,也就是当一个任务一直无限运行,所有周期全满窗,其负载可以无限接近 LOAD_AVG_PERIOD 值,其对于的计算公式为:

/*
 *                            inf
 *    LOAD_AVG_MAX  = 1024 * \Sum y^n
 *                            n=0 
 */

这个宏的值是 Documentation/scheduler/sched-pelt.c中 calc_converged_max() 计算得出的,其只计算了320个窗口(inf取320为47741.4),若inf取10000000(约单次满窗运行10000秒),得出的就是 47788

注:只要满足上面规律,LOAD_AVG_PERIOD 是可以该小的,比如 WALT 负载更新算法中就将其改小为8了。

 

7. 可以 cat /proc/<pid>/sched 看各个成员的状态,可以设置优先级和绑核,在系统idle状态下进行观察。

8. RT 任务的负载统计与权重无关,所以RT任务封装的负载统计 ___update_load_avg() 函数中权重和runnable的权重传的都是1,对于cfs的group se传的也是1,对于普通cfs任务的传的是其优先级对应的权重值。

9. MTK平台下做个实验

(1) 运行三个死循环
# while true; do let i=i+1; done &
[1] 25805# while true; do let i=i+1; done &
[2] 25806# while true; do let i=i+1; done &
[3] 25807

(2) 将三个死循环绑定在CPU6上
taskset -p 40 25805
taskset -p 40 25806
taskset -p 40 25807

(3) 优先级分别设置为 100,120,139
renice -n -20 -p 25805
renice -n 19 -p 25807

(4) 运行一段时间后cat
# cat /proc/25805/sched
se.load.weight                               :             90891264 //90891264 = 88761 * 1024,88761就是100优先级对应的权重值,也就是 sched_prio_to_weight[0] 的值,对权重还进行了scale_up()
se.avg.load_sum                              :           3335661917 //PELT算法下,这个为 Sum(权重 * 运行时间),由于在不断对历史的load_sum做衰减,100优先级的任务一直满跑,其最终会趋于一个定值,88761 * 47742 = 4237627662
se.avg.util_sum                              :             34445249 //PELT算法下,为scale_up后的运行时间的衰减累加值,与优先级无关,只与delta时间有关,其最终会趋向一个定值:1024 * 47742 = 48887808,说明见"注1"
se.avg.load_avg                              :                71239 //一直跑,就会接近其优先级对应的权重值,优先级和权重的对应关系见 sched_prio_to_weight[]
se.avg.util_avg                              :                  735 //是一个scale_up(就是<<10)后的运行时间的比值,定义为 running% * 1024,最大值为1024,此时其负载为 735/1024 = 71.8%。WALT算法中只要满跑,此域瞬间就达到1024
se.avg.last_update_time                      :       23338900368384
se.avg.util_est.ewma                         :                  320
se.avg.util_est.enqueued                     :                  732
policy                                       :                    0
prio                                         :                  100

# cat /proc/25806/sched se.load.weight : 1048576 //1048576 = 1024 * 1024,1024为120优先级对应的weight, se.avg.load_sum : 38715383 se.avg.util_sum : 2760254 se.avg.load_avg : 822 se.avg.util_avg : 58 se.avg.last_update_time : 23343228391424 se.avg.util_est.ewma : 0 se.avg.util_est.enqueued : 0 policy : 0 prio : 120

# cat /proc/25807/sched se.load.weight : 15360 //15360 = 15 * 1024,15为139优先级对应的weight, se.avg.load_sum : 573337 se.avg.util_sum : 2810605 se.avg.load_avg : 12 se.avg.util_avg : 58 se.avg.last_update_time : 23326280420352 se.avg.util_est.ewma : 0 se.avg.util_est.enqueued : 0 policy : 0 prio : 139

注1:根据util_avg 的定义:util_avg = running% * SCHED_CAPACITY_SCALE,和 ___update_load_avg() 中 WRITE_ONCE(sa->util_avg, sa->util_sum / divider); 其中 divider 取最大值为 47742,util_avg 取最大值为 1024,因此 util_sum 的最大值只能是 1024 * 47742 = 48887808,这个是scale_up的,也就是乘以1024后的值。

注2:若任务一直跑,其se.avg.load_avg就会接近其优先级对应的权重值,但是反过来并不成立,比如任务先一直运行一段时间,然后sleeping或被冻结住,此时其load_avg还没来得及更新,还是会一直保持接近其权重值,可以通过cat /proc/<PID>/status 看线程的状态来确定。

注3:上面实验中 se.load.weight 的统计结果是4.14内核上的,最大值趋近于 se.weight*LOAD_AVG_MAX。在kernel-4.19以及之后的内核版本,其统计方法变更了,se.load.weight 中不再考虑任务权重,只是runnable(+running)状态几何级数的累加值,最大是趋近于 LOAD_AVG_MAX。

 

10. 总结

1. se.avg.load_sum:PELT算法下,这个为 Sum(权重 * 运行时间),由于在不断对历史的 load_sum 做衰减,100优先级的任务一直满跑,其最终会趋于一个定值,sched_prio_to_weight[0] * LOAD_AVG_MAX,若是取 q = 0.5^(1/32), 最大值也就是 88761 * 47742 = 4237627662

2. load_avg 的定义:load_avg = runnable% * scale_load_down(load),更新见 ___update_load_avg(),计算公式简化为:runnable time / LOAD_AVG_MAX * sched_prio_to_weight[nice]。se.load.weight 是 sched_prio_to_weight[nice] 的值 scale_up 后的,也就是乘以1024后的。一般 scale_load_down(load) 传的load是scale_up后的,scale_down后就是sched_prio_to_weight[nice] 的值。若一个任务无限运行,其 load_avg 就等于其权重值,也就是 sched_prio_to_weight[nice] 的值。

3. util_avg 的定义:util_avg = running% * SCHED_CAPACITY_SCALE,更新见 ___update_load_avg(),计算公式简化为:running time / LOAD_AVG_MAX * SCHED_CAPACITY_SCALE。

 

 

补充

(1) RT 任务的负载统计与权重无关,所以RT任务封装的负载统计 ___update_load_avg() 函数中权重和runnable的权重传的都是1,对于cfs的group se传的也是1,对于普通cfs任务的传的是其优先级对应的权重值。

(2) MTK平台测试,一个优先级为100的cfs进程,死循环,跑在大核上其 PELT算法下 se.avg.util_avg 为788,跑在小核上为 220。正常应该为1024的,和 CPU 算力cpu_capacity文件数值也不匹配。原因是受到CPU算力影响,而且会随着CPU频点进行scale。

(3) load_sum 考虑了 running 和 runnable 时间,而 util_sum 只考虑了 running 时间。

(4) 5.10 GKI后的内核支持半衰期窗口个数配置为8还是32,通过如下方式配置

/*
chosen: chosen {
    bootargs = "pelt=8 ...";
};
*/
early_param("pelt", set_pelt); //pelt.c

 

 

优秀博文:

CFS线程调度机制分析: https://zhuanlan.zhihu.com/p/581575174  //内核工匠

 

posted on 2021-09-25 19:11  Hello-World3  阅读(1314)  评论(0编辑  收藏  举报

导航