• 调度器5—CFS负载计算1_PELT_不考虑CFS组调度和带宽控制 Hello


    基于Linux-5.10

    一、CFS负载简介

    1. 负载表示

    PELT算法下,CFS负载使用 struct sched_avg 结构表示。其内嵌在 struct sched_entity 和 struct cfs_rq 中,分别用来表示任务的负载和CPU的负载。

    2. 负载分类

    负载分为平均负载(load_avg)、平均运行负载(runnable_avg)、平均利用率(util_avg)。后文为了简单省略“平均”二字,直接称之为为负载、运行负载和利用率。

    3. 负载的使用

    (1) 平均负载(load_avg)

    负载均衡中若迁移任务的类型是 migrate_load,就使用各CPU的 cfs_rq->avg.load_avg 值找出最忙的CPU,见 find_busiest_queue()。然后迁移任务,逐步在不均衡量中减去 p->se.avg.load_avg 来达到均衡,见 detach_tasks()。

    (2) 平均运行负载(runnable_avg)

    主要作用于在两个位置,一是CFS选核的慢速路径中,通过影响 sg_lb_stats::group_type 的值来帮忙找出最idle的Cluster。二是在负载均衡路径中通过影响 sg_lb_stats::group_type 的值,进一步影响 lb_env::migration_type 的值来帮忙选出最忙的CPU。

    select_task_rq_fair //CFS任务选核慢速路径
        find_idlest_cpu
            find_idlest_group
                update_sg_wakeup_stats //更新 sg_lb_stats::group_runnable 值
                    cpu_runnable_without
                        cpu_runnable
                            cfs_rq_runnable_avg
                                return cfs_rq->avg.runnable_avg;
                    group_classify  //影响 sg_lb_stats::group_type 的值
                        group_has_capacity //使用 sg_lb_stats::group_runnable
                        group_is_overloaded //使用 sg_lb_stats::group_runnable
    
    load_balance
        find_busiest_group
            update_sd_lb_stats
                update_sg_lb_stats
                    group_classify //影响 sg_lb_stats::group_type 的值
                        group_has_capacity
                        group_is_overloaded
            calculate_imbalance //影响 lb_env::migration_type 的值

    (3) 平均利用率(util_avg)

    主要作用于在两个位置,一是CFS负载均衡中若迁移任务的类型是 migrate_util,就使用各CPU的 cfs_rq->avg.util_avg 值找出最忙的CPU,见 find_busiest_queue()。然后迁移任务,逐步在不均衡量中减去 p->se.avg.util_avg 来达到均衡,见 detach_tasks()。二是在调频上使用 rq->cfs.avg.util_avg 选出next freq。

    sugov_next_freq_shared
        sugov_get_util
            max(rq->cfs.avg.util_avg, rq->cfs.avg.util_est)

    二、相关结构

    1. struct sched_avg

    /*
     * load_avg/runnable_avg/util_avg 累积一个无限几何级数
     * (参见 kernel/sched/pelt.c 中的 __update_load_avg_cfs_rq())。
     *
     * [load_avg 定义]
     * load_avg = runnable% * scale_load_down(load)
     *
     * [runnable_avg 定义]
     * runnable_avg = runnable% * SCHED_CAPACITY_SCALE
     *
     * [util_avg 定义]
     * util_avg = running% * SCHED_CAPACITY_SCALE
     *
     * 其中 runnable% 是 sched_entity 可运行(runnable)的时间比率,
     * running% 是 sched_entity 运行(running)的时间比率。
     *
     * 对于 cfs_rq,它们是所有可运行(runnable)和阻塞(blocked)的
     * sched_entities 的聚合值。 
     *
     * load_avg/runnable_avg/util_avg 不直接考虑 frequency scale
     * 和 cpu capacity scale。 scale缩放是通过用于计算这些信号的
     * 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。
     */
    struct sched_avg { //include/linux/sched.h
        u64                last_update_time;
        u64                load_sum;
        u64                runnable_sum;
        u32                util_sum;
        u32                period_contrib;
        /* 各种负载 */
        unsigned long    load_avg;
        unsigned long    runnable_avg;
        unsigned long    util_avg;
    } ____cacheline_aligned;

    last_update_time: sched avg会定期更新,last_update_time 是上次更新的时间点,单位ns,更新函数见 ___update_load_sum()。结合当前的时间值,我们可以计算delta并更新 *_sum 和 *_avg。对task se而言,还有一种特殊情况就是迁移到新的CPU,这时候需要将 last_update_time 设置为0以便新cpu上可以重新开始计算负载,此外,新任务的 last_update_time 也会设置为0。若唤醒选的核和任务之前运行的cpu不是同一个,会进行唤醒迁移,此时需要将任务 p 的负载从原 cpu 的 cfs_rq 上删除,这个删除动作会先标记删除,然后将 p->se.avg.last_update_time = 0 来使目标 cfs_rq 感知这是迁移过来的任务,见 migrate_task_rq_fair()。若使能了组调度,在task group之间迁移任务也使用的是相同的机制,见 task_move_group_fair()。

    load_sum: accumulate_sum()计算出来的负载几何级数累加和,下文详细说明。

    runnable_sum: accumulate_sum()计算出来的可运行负载几何级数累加和,下文详细说明。

    util_sum: accumulate_sum()计算出来的利用率几何级数累加和,下文详细说明。

    period_contrib: 是一个中间计算变量,在更新负载的时候分三段,d1(合入上次更新负载的剩余时间,即不足1ms窗口的时间),d2(满窗时间),d3(不足1ms窗口的时间),period_contrib 记录了d3窗口的时间,单位us,方便合入下次的d1。

    load_avg: 定义为:load_avg = runnable% * scale_load_down(load)。对权重的考虑是在由 load_sum 计算 load_avg 的时候才带入的,最大值为任务的权重。

    runnable_avg: 定义为:runnable_avg = runnable% * SCHED_CAPACITY_SCALE(cfs_rq的runnable%使用其上挂的任务的个数进行表示)。是根据 runnable_sum 计算出来的均值。最大值为 SCHED_CAPACITY_SCALE。

    util_avg: 定义为:util_avg = running% * SCHED_CAPACITY_SCALE。是根据 util_sum 计算出来的均值。最大值为 SCHED_CAPACITY_SCALE。

    2. struct sched_entity

    struct sched_entity {
        struct load_weight    load;
        unsigned int        on_rq;
        /* 内嵌的 sched_avg 结构 */
        struct sched_avg    avg;
        ...
    }

    load: 调度实体的负载权重,对于task se,该值和nice value相关。

    on_rq: 表示此时是否挂在 cfs_rq 链表上,但是pick出去运行时虽然从cfs_rq上摘下来了,但是 se->on_rq 却没有设置为0,因此此成员表示se处于 running+runnable 状态。

    avg: 调度实体的负载。

    3. struct cfs_rq

    struct cfs_rq {
        struct load_weight    load;
        unsigned int        nr_running;
        unsigned int        h_nr_running;
        /* 内嵌的 sched_avg 结构 */
        struct sched_avg    avg;
        struct {
            raw_spinlock_t    lock ____cacheline_aligned;
            int        nr;
            unsigned long    load_avg;
            unsigned long    util_avg;
            unsigned long    runnable_avg;
        } removed;
        ...
    }

    load: 挂入该cfs rq所有调度实体的负载权重之和

    nr_running: cfs_rq上se的个数,只计算本层级的se个数,包括task se和group se。

    h_nr_running: cfs_rq上se的个数,包括group se子层级的上se个数。若没有使能组调度,同 nr_running 表示cfs_rq上se个数。

    avg: cfs_rq 的负载。

    removed: 当一个任务退出或者唤醒后迁移到到其他cpu上的时候,那么原本所在CPU的cfs rq上需要移除该任务带来的负载。由于持rq锁问题,所以先把移除的负载记录在这个 removed 成员中,适当的时机再更新之。删除记录函数 remove_entity_load_avg(),实际删除函数 update_cfs_rq_load_avg()。

    三、相关变量

    1. sched_prio_to_weight[] 数组

    const int sched_prio_to_weight[40] = { //core.c
     /* -20 */     88761,     71755,     56483,     46273,     36291,
     /* -15 */     29154,     23254,     18705,     14949,     11916,
     /* -10 */      9548,      7620,      6100,      4904,      3906,
     /*  -5 */      3121,      2501,      1991,      1586,      1277,
     /*   0 */      1024,       820,       655,       526,       423,
     /*   5 */       335,       272,       215,       172,       137,
     /*  10 */       110,        87,        70,        56,        45,
     /*  15 */        36,        29,        23,        18,        15,
    };

    此数组对应任务的权重,和优先级的对应关系为 weight = 1024 / (1.25 ^ nice)。nice值每减去1,就表示相同条件下多获取25%的时间片(不考虑任务权重变化对总权重的影响)。任务 sched_entity::load::weight 就对应这里scale up后的权重值。

    2. LOAD_AVG_PERIOD 宏变量

    衰减 LOAD_AVG_PERIOD 个周期后,负载变为之前的一半。取值32表示 y^32=0.5。也就是衰减因子 y 等于 0.5 开 LOAD_AVG_PERIOD 次方。

    3. LOAD_AVG_MAX 宏变量

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

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

    任务满负载跑的话,其运行时间几何级数累加的最大值。其中1024是1ms近似取1024us。注意,这是没有scale up的,只是一个几何级数。

    4. pelt_runnable_avg_yN_inv[]

    pelt_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。pelt_runnable_avg_yN_inv[] 中一共有 LOAD_AVG_PERIOD 个元素。

    //sched-pelt.h
    /* Generated by Documentation/scheduler/sched-pelt; do not modify. */
    
    static const u32 pelt32_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 PELT32_LOAD_AVG_PERIOD 32
    #define PELT32_LOAD_AVG_MAX 47742
    
    static const u32 pelt8_runnable_avg_yN_inv[] __maybe_unused = {
        0xffffffff, 0xeac0c6e6, 0xd744fcc9, 0xc5672a10,
        0xb504f333, 0xa5fed6a9, 0x9837f050, 0x8b95c1e3,
    };
    
    #define PELT8_LOAD_AVG_PERIOD 8
    #define PELT8_LOAD_AVG_MAX 12336
    
    #define LOAD_AVG_PERIOD pelt_load_avg_period //等效 #define LOAD_AVG_PERIOD 32
    #define LOAD_AVG_MAX pelt_load_avg_max       //等效 #define LOAD_AVG_MAX 47742
    
    
    //pelt.c
    int pelt_load_avg_period = PELT32_LOAD_AVG_PERIOD;
    int pelt_load_avg_max = PELT32_LOAD_AVG_MAX;
    const u32 *pelt_runnable_avg_yN_inv = pelt32_runnable_avg_yN_inv;

    PELT周期默认为32,也支持周期配置为8,通过内核启动参数 "pelt=8" 进行修改。

    可以在Excel中通过 POWER(0.5, 1/32) 来计算0.5开32次方的值。当累加到 y^32 次方时 LOAD_AVG_MAX 可取值 24406,当累加到 y^500 次方时 LOAD_AVG_MAX 可取值 47787。通过 DEC2HEX() 和 HEX2DEC() 来验证数组中的每一个元素。

    5. 常用的 divider

    #define PELT_MIN_DIVIDER    (LOAD_AVG_MAX - 1024)
    
    static inline u32 get_pelt_divider(struct sched_avg *avg) //pelt.h
    {
        /* avg->period_contrib 单位us */
        return PELT_MIN_DIVIDER + avg->period_contrib;
    }

    函数返回值为 LOAD_AVG_MAX - 1024 - avg->period_contrib,表示历史时间的几何级数,由于最后一个1024us的周期中只占用了 avg->period_contrib,因此这样计算。


    四、PELT中使用的timeline

    1. struct rq 中有如下timeline:

    struct rq {
        ...
        u64    clock;
        u64    clock_task ____cacheline_aligned;
        u64    clock_pelt;
        ...
        unsigned long    lost_idle_time;
    };

    clock: 基于 sched clock 的时间,通过 update_rq_clock() 不断驱动这个 timeline 向前推进。通常通过 rq_clock() 来访问。

    clock_task: 这个 timeline 类似上面的 clock,只不过当执行 irq 的时候,clock_task 会停掉,因此,这个 clock 只会随着任务的执行而不断向前推进。具体更新方法可以参考 update_rq_clock_task()。通常通过 rq_clock_task(rq) 访问。

    clock_pelt: PELT用于计算负载的 timeline,这个时间是 clock_task 归一化后的时间(上面两个都是未归一化的物理时间)。当CPU idle的时候,clock_pelt 会同步到 clock_task 上去,负载更新函数见 update_rq_clock_pelt()。通常通过 cfs_rq_clock_pelt(cfs_rq) 来访问。后两个timeline的唯一调用路径:update_rq_clock() --> update_rq_clock_task() --> update_rq_clock_pelt()。只有 update_rq_clock() 通过 EXPORT_SYMBOL_GPL 导出,所有使用这三个clock的地方都应该先调用 update_rq_clock() 更新这三个时间线。

    2. update_rq_clock_pelt() 函数更新 rq->clock_pelt 时间线

    最终计算的 delta= delta * (capacity_cpu / capacity_max(1024)) * (cur_cpu_freq / max_cpu_freq)。 也就是将当前cpu在当前频点上运行得到的delta值,缩放到最大性能CPU的最大频点上对应的delta值。然后累加到 clock_pelt 上。idle时同步到 clock_task 上去。

    /*
     * clock_pelt 缩放时间以反映在运行增量时间期间完成的有效计算量,然后
     * 在 rq 空闲时同步回 clock_task。
     *
     * 实际的任务执行时间delta,通过cpu scale和freq scale,归一化到了系
     * 统最大算力的CPU上去。
    * idle时同步到 clock_task 上去。
    */ static inline void update_rq_clock_pelt(struct rq *rq, s64 delta) //pelt.h { if (unlikely(is_idle_task(rq->curr))) { /* The rq is idle, we can sync to clock_task */ rq->clock_pelt = rq_clock_task(rq); return; } /* delta * capacity_cpu / capacity_max(1024) */ delta = cap_scale(delta, arch_scale_cpu_capacity(cpu_of(rq))); /* delta * cur_freq / max_freq */ delta = cap_scale(delta, arch_scale_freq_capacity(cpu_of(rq))); /* * 最终计算的 delta= delta * (capacity_cpu / capacity_max(1024)) * (cur_cpu_freq / max_cpu_freq) * 也就是将当前cpu在当前频点上运行得到的delta值,缩放到最大性能CPU的最大频点上对应的delta值。然后 * 累加到 clock_pelt 上。 */ rq->clock_pelt += delta; }

    3. PELT负载跟踪中使用的时间线是 rq->clock_pelt,衰减时也使用的是这个timeline。CFS、RT、Deadline 中负载的更新使用的都是这个timeline。

    static inline u64 cfs_rq_clock_pelt(struct cfs_rq *cfs_rq) //pelt.h
    {
        return rq_clock_pelt(rq_of(cfs_rq));
    }
    
    static inline u64 rq_clock_pelt(struct rq *rq) //pelt.h
    {
        /* 这里应该等于返回 rq->clock_pelt */
        return rq->clock_pelt - rq->lost_idle_time;
    }

    rq->lost_idle_time 更新逻辑:

    static inline void update_idle_rq_clock_pelt(struct rq *rq)
    {
        u32 divider = ((LOAD_AVG_MAX - 1024) << SCHED_CAPACITY_SHIFT) - LOAD_AVG_MAX; //47791490
        /* cfs、rt、dl   所有任务的 util_sum 值之和 */
        u32 util_sum = rq->cfs.avg.util_sum;
        util_sum += rq->avg_rt.util_sum;
        util_sum += rq->avg_dl.util_sum;
    
        /*
         * util_sum 最大值 1024 * 47742 = 48887808,单从这里看,
         * 的确可能是出现大于 divider 的情况的。
         */
        if (util_sum >= divider)
            rq->lost_idle_time += rq_clock_task(rq) - rq->clock_pelt;  //rq->clock_task
    }

    唯一调用路径:

    pick_next_task_fair //即使 newidle_balance 也没有拉过来任务才执行
        update_idle_rq_clock_pelt //pelt.h

    rq都已经idle了,util_sum 却还非常大,应该属于一种异常情况,正常 rq->lost_idle_time 应该恒等于0。

    4. rq->clock 和 rq->clock_task 两个时间线 sched_debug 中可以查看其值。

    # cat /proc/sched_debug
    ...
    cpu#7
      .clock              : 90418888.572642
      .clock_task        : 90402493.094804

    五、负载计算

    1. 任务负载初始化

    load avg的初始化分成两个阶段,第一个阶段在创建 sched entity 的时候,对于task se而言就是在fork的时候,调用 init_entity_runnable_average() 完成初始化。第二个阶段在唤醒这个新创建se的时候,可以参考 post_init_entity_util_avg() 函数的实现。

    void init_entity_runnable_average(struct sched_entity *se) //fair.c
    {
        struct sched_avg *sa = &se->avg;
    
        memset(sa, 0, sizeof(*sa));
    
        /* 只对task se进行初始化,初始化为最大值,即权重值 */
        if (entity_is_task(se))
            sa->load_avg = scale_load_down(se->load.weight);
    }
    
    void post_init_entity_util_avg(struct task_struct *p) //fair.c
    {
        struct sched_entity *se = &p->se;
        struct cfs_rq *cfs_rq = cfs_rq_of(se);
        struct sched_avg *sa = &se->avg;
        /* 不考虑限频的原始cpu算力 */
        long cpu_scale = arch_scale_cpu_capacity(cpu_of(rq_of(cfs_rq)));
        /* 计算cpu剩余算力的一半 */
        long cap = (long)(cpu_scale - cfs_rq->avg.util_avg) / 2;
    
        if (cap > 0) {
            if (cfs_rq->avg.util_avg != 0) {
                /* 优先级越高,初始util值就越大 */
                sa->util_avg  = cfs_rq->avg.util_avg * se->load.weight;
                sa->util_avg /= (cfs_rq->avg.load_avg + 1);
    
                /* 限制max到剩余算力的一半 */
                if (sa->util_avg > cap)
                    sa->util_avg = cap;
            } else {
                sa->util_avg = cap;
            }
        }
    
        /* runnable_avg 初始化和 util_avg 相等的值 */
        sa->runnable_avg = sa->util_avg;
        ...
        /* 将se的负载传导到cfs_rq上 */
        attach_entity_cfs_rq(se);
    }

    2. 负载更新

    2.1 负载更新函数是 update_load_avg(), 它更新task se 和 cfs_rq 的负载。调用路径和传参如下:

    //从tse触发的都是down-top进行遍历
    
    enqueue_task_fair //for_each_sched_entity(se)
        enqueue_entity(cfs_rq, se, flags)
            update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
    
    dequeue_task_fair //for_each_sched_entity(se)
        dequeue_entity(cfs_rq, se, flags)
            update_load_avg(cfs_rq, se, UPDATE_TG);
    
    task_tick_fair //for_each_sched_entity(se)
        entity_tick(cfs_rq, curr, queued)
            update_load_avg(cfs_rq, curr, UPDATE_TG);
    
    enqueue_task_fair //for_each_sched_entity(se)
        update_load_avg(cfs_rq, se, UPDATE_TG);
    
    dequeue_task_fair //for_each_sched_entity(se)
        update_load_avg(cfs_rq, se, UPDATE_TG);
    
    
    //从rq触发的都是top-down的遍历
    
    pick_next_task_fair //for_each_sched_entity(se)
        put_prev_entity(cfs_rq, prev) //if (prev->on_rq)为真才调用
            update_load_avg(cfs_rq, prev, 0);
        set_next_entity(cfs_rq, se) //if (se->on_rq)为真才调用
            update_load_avg(cfs_rq, se, UPDATE_TG);
    
    
    //这两个是带宽控制的
    throttle_cfs_rq
        update_load_avg(qcfs_rq, se, 0);
    unthrottle_cfs_rq
        update_load_avg(cfs_rq, se, UPDATE_TG);

    2.2 update_load_avg()

    这是负载更新的主要函数,通过参数 flags 来指定更新需求。

    static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
    {
        u64 now = cfs_rq_clock_pelt(cfs_rq); //使用的是 rq->clock_pelt
        int decayed;
    
        /*
         * 只有 attach_entity_cfs_rq 可能传 SKIP_AGE_LOAD,但是恒不会传。
         *
         * 非迁移路径才需要更新se的负载,last_update_time=0的迁移中已经
         * 设定好了,不需要再更新了。
         */
        if (se->avg.last_update_time && !(flags & SKIP_AGE_LOAD))
            /* (1) 更新本层级 sched entity 的 load avg */
            __update_load_avg_se(now, cfs_rq, se);
    
        /* (2) 更新该se挂入的cfs rq的load avg */
        decayed  = update_cfs_rq_load_avg(now, cfs_rq);
        /* (3) 不使能组调度就是空函数 */
        decayed |= propagate_entity_load_avg(se);
    
        /*
         * 只有 enqueue_entity 路径进来的传参中才有 DO_ATTACH 标志,
         * 二者结合说明是任务迁移场景。 
         */
        if (!se->avg.last_update_time && (flags & DO_ATTACH)) {
    
            /*
             * DO_ATTACH means we're here from enqueue_entity().
             * !last_update_time means we've passed through
             * migrate_task_rq_fair() indicating we migrated.
             *
             * IOW we're enqueueing a task on a new CPU.
             */
            /*
             * (4) 将se的load avg和load sum(load、runnable load 和 util)
             * 加到cfs rq上去.
             */
            attach_entity_load_avg(cfs_rq, se);
            /* (5) 若不使能组调度就是一个空函数 */
            update_tg_load_avg(cfs_rq);
    
        /* 至少衰减一个PELT周期(1ms)了,检查是否要调频 */
        } else if (decayed) {
            /* 负载衰减了,检查是否需要调频 */
            cfs_rq_util_change(cfs_rq, 0);
    
            if (flags & UPDATE_TG)
                /* (6) 若不使能组调度就是一个空函数 */
                update_tg_load_avg(cfs_rq);
        }
    }
    2.2.1 __update_load_avg_se()

    该函数被 update_load_avg() 唯一调用,用于更新se负载。注意,若是没有经历完整周期的话就只更新 *_sum 值,不更新 *_avg 值

    int __update_load_avg_se(u64 now, struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
        /* 1. 先求 *_sum 值。若经历了完整周期返回1 */
        if (___update_load_sum(now, &se->avg, !!se->on_rq, se_runnable(se), cfs_rq->curr == se)) {
    
            /* 2. 若经历了完整周期,求平均值 *_avg */
            ___update_load_avg(&se->avg, se_weight(se));
            /* 3. 若经历了完整周期,触发调频 */
            cfs_se_util_change(&se->avg);
            trace_pelt_se_tp(se);
            return 1;
        }
    
        return 0;
    }
    2.2.1.1 ___update_load_sum()

    (1) 更新se负载传参:

    /*
     * __update_load_avg_se: (now, &se->avg, !!se->on_rq, se_runnable(se), cfs_rq->curr == se)
     * 参数:
     *    now: rq->clock_pelt
     *  !!se->on_rq: 对于running+runnable状态的任务为1,sleep状态为0。
     *  se_runnable(se): 不考虑组调度就是 !!se->on_rq。
     *  cfs_rq->curr == se: 对于running状态的任务为1,sleep+runnable为0。
     */

    (2) 更新cfs_rq负载传参:

    /*
     * (2) 更新 cfs_rq 负载传参:
     * __update_load_avg_cfs_rq: (now, &cfs_rq->avg, scale_load_down(cfs_rq->load.weight), cfs_rq->h_nr_running, cfs_rq->curr != NULL)
     * 参数:
     *  scale_load_down(cfs_rq->load.weight): cfs_rq的权重,是其上挂的所有se的权重之和。
     *  cfs_rq->h_nr_running: 若没有使能组调度,则表示cfs_rq上挂的所有se的个数,也就是runnable+running 任务个数(running状态的任务也包含在其中的)。
     *  cfs_rq->curr != NULL: 若没有使能组调度,则应该是恒为真的。
     *
     */

    此函数主要用于计算 *_sum 值。注意更新se负载和cfs_rq的负载都会调用它

    /*
     * 我们可以将可运行平均值的历史贡献表示为几何级数的系数。 为此,我们将可运行
     * 历史细分为大约 1 毫秒(1024 微秒)的片段; 标记 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) 的一半。
     *
     * 当一个周期翻转"rolls over"并且我们有新的 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 + ... [重新标记 u_i --> u_{i+1}]
     *
     */
    
    /* 返回值:经历了完整周期,返回1,否则返回0 */
    static __always_inline int ___update_load_sum(u64 now, struct sched_avg *sa,
        unsigned long load, unsigned long runnable, int running)
    {
        u64 delta;
    
        /* 此时delta单位是ns */
        delta = now - sa->last_update_time;
        /*
         * 这应该只在时间倒退时发生,不幸的是,当我们切换到 TSC 时,
         * 它会在 sched clock init 期间发生。
         */
        if ((s64)delta < 0) {
            sa->last_update_time = now;
            return 0;
        }
    
        /*
         * 为了快速计算,使用1024ns近似为1us, delta不足1us的不更新。
         * 右移后delta单位为us.
         */
        delta >>= 10;
        if (!delta)
            return 0;
    
        /* 左移回去后赋值,sa->last_update_time 的单位ns */
        sa->last_update_time += delta << 10;
    
        /*
         * 翻译:
         * running 是 runnable (weight) 的子集,因此 runnable 被清理了 running 也
         * 不能设置。但是有极端的情况,就是 current se 已经 dequeue 了,但是
         * cfs_rq->curr 仍然指向它。这意味着权重将为 0,但不会为 sched_entity 运行,
         * 但如果 cfs_rq 变得空闲,它也不会运行。 例如,这发生在调用
         * update_blocked_averages() 的 idle_balance() 期间。
         *
         * 另请参阅 accumulate_sum() 中的注释。
         */
        if (!load)
            runnable = running = 0;
    
        /*
         * 现在我们知道我们跨越了测量单位的界限。 *_avg 通过两个步骤累积:
         *
         * 第 1 步:从 last_update_time 开始累积 *_sum。 如果我们还没有跨
         * 越时期界限,那就结束吧。
         */
        if (!accumulate_sum(delta, sa, load, runnable, running))
            return 0;
    
        return 1;
    }
    2.2.1.1.1 accumulate_sum()
    static __always_inline u32 accumulate_sum(u64 delta, struct sched_avg *sa,
               unsigned long load, unsigned long runnable, int running) //pelt.c
    {
        /* 单位us */
        u32 contrib = (u32)delta; /* p == 0 -> delta < 1024 */
        u64 periods;
    
        /* 此时 sa->period_contrib 还是上次的d3,这里要和d1凑足一个周期 */
        delta += sa->period_contrib;
        /* 除以1024后单位ms,经历的整周期数 */
        periods = delta / 1024; /* A period is 1024us (~1ms) */
    
        /*
         * Step 1: decay old *_sum if we crossed period boundaries.
         */
        if (periods) {
            /* 乘以 y^periods 来衰减 periods 个周期 */
            sa->load_sum = decay_load(sa->load_sum, periods);
            sa->runnable_sum = decay_load(sa->runnable_sum, periods);
            sa->util_sum = decay_load((u64)(sa->util_sum), periods);
    
            /*
             * Step 2
             */
            /* 得到不足一个周期的部分,单位us */
            delta %= 1024;
            /*
             * task se 传参 load 为 !!se->on_rq。若load传0,那么外层函数已经将
             * runnable = running = 0 了,那就任何负载都更新不了。也就是说任务
             * 得是running或runable状态才能更新 *_sum。
             *
             * cfs_rq se 传参 load 为 scale_load_down(cfs_rq->load.weight),也
             * 就是说cfs_rq不能是idle的才会更新 *_sum。
             */
            if (load) {
                /*
                 * 翻译:这取决于:
                 * if (!load)
                 *    runnable = running = 0;
                 *
                 * 来自 ___update_load_sum(), 这导致下面对 @contrib 的
                 * 使用完全消失,因此计算它没有意义。
                 */
                contrib = __accumulate_pelt_segments(periods, 1024 - sa->period_contrib, delta);
            }
        }
    
        /* 将不足一个周期的d3缓存起来,以便下次和新的d1组合成一个新的周期 */
        sa->period_contrib = delta;
    
        /* 这里体现了 *_sum 的统计方法 */
        if (load)
            sa->load_sum += load * contrib;
        if (runnable)
            sa->runnable_sum += runnable * contrib << SCHED_CAPACITY_SHIFT;
        if (running)
            sa->util_sum += contrib << SCHED_CAPACITY_SHIFT;
    
        return periods;
    }

    (1) 对于task se:

    load_sum: 是对任务 running+runnable 状态的几何级数累加值。对于一个死循环,此值趋近于 LOAD_AVG_MAX 。

    runnable_sum: 是对任务 running+runnable 状态的几何级数累加值然后scale up后的值。对于一个死循环,此值趋近于 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE 。

    util_sum: 是对任务 running 状态的几何级数累加值然后scale up后的值。对于一个独占某个核的死循环,此值趋近于 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE,若不能独占,会比此值小。

    小结:

    load_sum 竟然没有考虑任务的权重!对任务权重的考虑放到了计算 load_avg 的时候使用了。runnable_sum 的值是 scale up 后的 load_sum 值。

    util_sum 与 runnable_sum 越接近,说明任务 running 状态占比越多,反之说明 runable 占比越多。

    (2) 对于cfs_rq:

    load_sum: cfs_rq 的 weight,也就是其上所有se的权重之和 \Sum(sched_prio_to_weight[]) 乘以非idle状态下的几何级数。

    runnable_sum: cfs_rq 上 runnable 状态(runnable+running)任务个数乘以非idle状态下的几何级数,然后再乘以 SCHED_CAPACITY_SCALE 后的值。

    util_sum: cfs_rq 上所有任务 running 状态下的几何级数之和再乘以 SCHED_CAPACITY_SCALE 后的值。

    2.2.1.1.1.1 decay_load()

    decay_load 接收两个参数:负载值val和衰减周期n,具体的逻辑如下:

    (1) LOAD_AVG_PERIOD(默认为32)x 63个周期之后,负载32ms之后衰减一半。0.5^63足够小可以直接忽略,因此衰减的结果就是0
    (2) 如果n没有那么大,那么我们分成两步衰减,一步是 LOAD_AVG_PERIOD 的整数倍的时间窗口衰减,即每32个周期,通过左移衰减一半。
    (3) 第二步是非 LOAD_AVG_PERIOD 的整数倍的时间窗口衰减,这部分可以通过查表获得。

    实际上,在 decay_load 中并没有正面计算几何序列,而是通过优化让 decay_load 非常的轻盈。

    static u64 decay_load(u64 val, u64 n) //pelt.c
    {
        unsigned int local_n;
    
        if (unlikely(n > LOAD_AVG_PERIOD * 63)) 
            return 0;
    
        /* after bounds checking we can collapse to 32-bit */
        local_n = n;
    
        /*
         * As y^PERIOD = 1/2, we can combine
         *    y^n = 1/2^(n/PERIOD) * y^(n%PERIOD)
         * With a look-up table which covers y^n (n<PERIOD)
         *
         * To achieve constant time decay_load.
         */
        if (unlikely(local_n >= LOAD_AVG_PERIOD)) {
            val >>= local_n / LOAD_AVG_PERIOD;
            local_n %= LOAD_AVG_PERIOD;
        }
    
        /* (val * pelt_runnable_avg_yN_inv[local_n]) >> 32 */
        val = mul_u64_u32_shr(val, pelt_runnable_avg_yN_inv[local_n], 32);
    
        return val;
    }
    2.2.1.1.1.2 __accumulate_pelt_segments()

    此函数用来计算几何级数。

    /*
     * accumulate_sum: (periods, 1024 - sa->period_contrib, delta)
     * 参数:delta跨越的周期数、d1、d3
     */
    static u32 __accumulate_pelt_segments(u64 periods, u32 d1, u32 d3) //pelt.c
    {
        /* d3在当前周期,不需要衰减 */
        u32 c1, c2, c3 = d3; /* y^0 == 1 */
    
        /*
         * c1 = d1 y^p
         *
         * d1 经历了完整的 periods 个周期,直接衰减 periods 个周期
         */
        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
         *
         * c2的周期级数累加是通过换算得来的。注意,上面的1024表示的是
         * 1ms~=1024us, 而不是scale up。
         */
        c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024;
    
        return c1 + c2 + c3;
    }
    2.2.1.2 ___update_load_avg

    该函数用于更新平均负载,通过此函数可以看到平均负载是如何计算出来的。

    /*
     * (1) 对于 task se:
     * __update_load_avg_se: (&se->avg, se_weight(se)) 
     * 参数2是 scale_load_down(se->load.weight),对task se就是 sched_prio_to_weight[] 对应的值。
     *
     * (2) 对于 cfa_rq:
     * __update_load_avg_cfs_rq: (&cfs_rq->avg, 1)
     * 注意更新cfs_rq平均负载时load传的是1,表示 load_avg=sa->load_sum/divider
     */
    static __always_inline void ___update_load_avg(struct sched_avg *sa, unsigned long load)
    {
        /* 历史时间级数,divider 取值 LOAD_AVG_MAX - 1024 + avg->period_contrib */
        u32 divider = get_pelt_divider(sa);
    
        /* Step 2: update *_avg. */
        sa->load_avg = div_u64(load * sa->load_sum, divider);
        sa->runnable_avg = div_u64(sa->runnable_sum, divider);
        WRITE_ONCE(sa->util_avg, sa->util_sum / divider);
    }

    (1) 对于task se:

    load_avg: 等于 sched_prio_to_weight[] * load_sum / divider, 就是 weight * load_sum / divider。由于 load_sum 是任务 running+runnable 状态的几何级数,divider 是几何级数,因此一个死循环任务的 load_avg 接近于其权重。

    runnable_avg: 等于 runnable_sum / divider。由于 runnable_sum 是任务 runnable+running 状态的几何级数scale up后的值,divider 几何级数,因此一个死循环任务的 runnable_avg 接近于 SCHED_CAPACITY_SCALE 。

    util_avg: 等于 util_sum / divider。由于 util_sum 是任务 running 状态的几何级数scale up后的值,divider 几何级数,因此一个死循环任务的 util_avg 接近于 SCHED_CAPACITY_SCALE 。

    (2) 对于cfs_rq:

    load_avg: 直接等于 load_sum / divider。cfs_rq 跑满(跑一个死循环或多个死循环),趋近于scal down后的cfs_rq的权重,也就是其上挂的所有调度实体的权重之和,即 \Sum(sched_prio_to_weight[])。

    runnable_avg: 等于 runnable_sum / divider。cfs_rq 跑满(跑一个死循环或多个死循环),趋近于cfs_rq上任务个数乘以 SCHED_CAPACITY_SCALE。

    util_avg: 等于 util_sum / divider。cfs_rq 跑满(跑一个死循环或多个死循环),趋近于 SCHED_CAPACITY_SCALE。

    对于cfs_rq,load_avgrunnable_avgutil_avg分别从权重(优先级)、任务个数、CPU时间片占用三个维度来描述CPU的负载。

    2.2.2 update_cfs_rq_load_avg()

    此函数用于更新 cfs_rq 的负载。

    /*
     * update_load_avg  传参 (now, cfs_rq)
     * 如果负载有周期衰减或移除了负载,则返回 true,否则返回false。
     */
    static inline int update_cfs_rq_load_avg(u64 now, struct cfs_rq *cfs_rq)
    {
        unsigned long removed_load = 0, removed_util = 0, removed_runnable = 0;
        struct sched_avg *sa = &cfs_rq->avg;
        int decayed = 0;
    
        /*
         * 当一个任务退出或者唤醒后迁移到到其它cpu上的时候,那么之前cpu的cfs_rq上
         * 需要移除该任务带来的负载。由于持rq锁问题,remove_entity_load_avg() 会先
         * 把要移除的负载记录在 cfs_rq->removed 成员中,在这里更新cfs_rq的负载的时
         * 候再删除。
        */
        if (cfs_rq->removed.nr) {
            unsigned long r;
            u32 divider = get_pelt_divider(&cfs_rq->avg);
    
            raw_spin_lock(&cfs_rq->removed.lock);
            /* 值交换,将原removed的成员清0 */
            swap(cfs_rq->removed.util_avg, removed_util);
            swap(cfs_rq->removed.load_avg, removed_load);
            swap(cfs_rq->removed.runnable_avg, removed_runnable);
            cfs_rq->removed.nr = 0;
            raw_spin_unlock(&cfs_rq->removed.lock);
    
            /* 对于cfs_rq load_avg=load_sum/divider */
            r = removed_load;
            sub_positive(&sa->load_avg, r);
            sub_positive(&sa->load_sum, r * divider);
    
            /* cfs_rq的 util_avg 也等于 util_sum/divider, 和task se是保持一致的 */
            r = removed_util;
            sub_positive(&sa->util_avg, r);
            sub_positive(&sa->util_sum, r * divider);
            /*
             * 翻译:由于四舍五入,se->util_sum 最终可能比 cfs->util_sum 多 +1。 尽管这本身不是问题,
             * 但在 util_avg 的 2 次更新(~1ms)之间分离大量具有舍入问题的任务可以使 cfs->util_sum 
             * 变为空,而 cfs_util_avg 则不是。
             * 检查 util_sum 是否仍高于新 util_avg 的下限。 鉴于 period_contrib 自上次同步以来可能
             * 已经移动,我们只能确定 util_sum 必须高于或等于 util_avg * 最小可能除法器(不算当前周
             * 期的分量)
             */
            sa->util_sum = max_t(u32, sa->util_sum, sa->util_avg * PELT_MIN_DIVIDER);
    
            /* cfs_rq 的 runnable_avg 也等于 runnable_sum/divider, 和 task se是保持一致的 */
            r = removed_runnable;
            sub_positive(&sa->runnable_avg, r);
            sub_positive(&sa->runnable_sum, r * divider);
    
            /*
             * 翻译:removed_runnable 是 removed_load 的未加权版本,因此我们可
             * 以使用它来估计 removed_load_sum。
             */
            /* 若不使能组调度就是空函数 */
            add_tg_cfs_propagate(cfs_rq, -(long)(removed_runnable * divider) >> SCHED_CAPACITY_SHIFT);
    
            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
    
        return decayed;
    }
    2.2.2.1 __update_load_avg_cfs_rq()

    此函数用于更新cfs_rq的负载,同task se一样,也调用了下面两个函数,注意参数的不同。cfs_rq的负载见上文中对这两个函数的分析。

    int __update_load_avg_cfs_rq(u64 now, struct cfs_rq *cfs_rq)
    {
        if (___update_load_sum(now, &cfs_rq->avg, scale_load_down(cfs_rq->load.weight),
                cfs_rq->h_nr_running, cfs_rq->curr != NULL)) {
    
            ___update_load_avg(&cfs_rq->avg, 1);
            trace_pelt_cfs_tp(cfs_rq);
            return 1;
        }
    
        return 0;
    }

    不考虑组调度的话,cfs_rq的权重就是其上挂载的所有se的权重,更新如下:

    enqueue_entity
        account_entity_enqueue(cfs_rq, se);
            update_load_add(&cfs_rq->load, se->load.weight)
                lw->weight += inc;
    
    dequeue_entity
        account_entity_dequeue
            update_load_sub(&cfs_rq->load, se->load.weight);
                lw->weight -= dec;
    2.2.3 attach_entity_load_avg()

    更新负载时,对于唤醒迁移过来的任务,需要将其负载加到当前 cfs_rq 上。本cpu上休眠后唤醒的任务却不需要添加。

    /**
     * attach_entity_load_avg - attach this entity to its cfs_rq load avg
     * @cfs_rq: cfs_rq to attach to
     * @se: sched_entity to attach
     *
     * 必须在此之前调用 update_cfs_rq_load_avg(),因为我们依赖 cfs_rq->avg.last_update_time
     * 是最新的。
     */
    static void attach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
        /*
         * cfs_rq->avg.period_contrib can be used for both cfs_rq and se.
         * See ___update_load_avg() for details.
         */
        u32 divider = get_pelt_divider(&cfs_rq->avg);
    
        /*
         * 翻译:当我们将@se附加到@cfs_rq时,我们必须对齐衰减窗口,若不对齐,
         * 可能会发生非常奇怪和奇妙的事情。
         */
        se->avg.last_update_time = cfs_rq->avg.last_update_time;
        se->avg.period_contrib = cfs_rq->avg.period_contrib;
    
        /*
         * 翻译:我们需要根据新的 period_contrib 重新计算 _sum。
         * 这并不完全正确,但由于我们完全在 PELT 层次结构之外,所以没有人
         * 关心我们是否稍微截断 *_sum。
         */
        /*
         * 补充 task se:
         * runnable_avg = runnable_sum / divider
         * util_avg = util_sum / divider
         * load_avg = sched_prio_to_weight[prio] * load_sum / divider
         */
        se->avg.util_sum = se->avg.util_avg * divider;
        se->avg.runnable_sum = se->avg.runnable_avg * divider;
        se->avg.load_sum = divider;
        if (se_weight(se)) {
            /* sched_prio_to_weight[] * load_sum / divider * divider / sched_prio_to_weight[] == load_sum */
            se->avg.load_sum = div_u64(se->avg.load_avg * se->avg.load_sum, se_weight(se));
        }
    
        /* 
         * enqueue_load_avg 执行:
         * cfs_rq->avg.load_avg += se->avg.load_avg;
         * cfs_rq->avg.load_sum += se_weight(se) * se->avg.load_sum;
         *
         * 补充:
         * task se 的 load_sum 是对任务 running+runnable 状态的几何级数累加值。
         * cfs_rq 的 load_sum 是其上所有se的权之和乘以非idle状态下的几何级数
        */
        enqueue_load_avg(cfs_rq, se);
        cfs_rq->avg.util_avg += se->avg.util_avg;
        cfs_rq->avg.util_sum += se->avg.util_sum;
        cfs_rq->avg.runnable_avg += se->avg.runnable_avg;
        cfs_rq->avg.runnable_sum += se->avg.runnable_sum;
    
        /* 不使能组调度的话是空函数 */
        add_tg_cfs_propagate(cfs_rq, se->avg.load_sum);
    
        cfs_rq_util_change(cfs_rq, 0);
    
        trace_pelt_cfs_tp(cfs_rq);
    }

    可以看到 load_avg/runnable_avg/util_avg/runnable_sum/util_sum 是直接相加的。load_sum 是任务的先乘以其权重后再相加的,这是两者计算方式的差异导致的,task se的 load_sum 没有考虑权重,在计算 load_avg 时才把权重考虑进去。

    其它调用路径:

            wake_up_new_task //唤醒一个新建任务
                post_init_entity_util_avg
    rt_mutex_setprio
    __sched_setscheduler
        check_class_changed //更改p的调度类,且p之前是CFS任务时调用
            switched_to_fair
        task_change_group_fair //使能组调度时组间迁移路径    
            task_move_group_fai
                attach_task_cfs_rq
        cpu_cgroup_css_online //某个task geoup的oneline路径
            sched_online_group
                online_fair_sched_group
                    attach_entity_cfs_rq
                        attach_entity_load_avg(cfs_rq, se)

    六、任务迁移对负载影响

    1. 当任务迁移来、或从非CFS调度类变为CFS调度类、或新创建CFS任务投入运行,都会调用 attach_entity_load_avg() 函数将其负载纳入cfs_rq的负载上。

    static void attach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
        u32 divider = get_pelt_divider(&cfs_rq->avg);
    
        se->avg.util_sum = se->avg.util_avg * divider;
        se->avg.runnable_sum = se->avg.runnable_avg * divider;
        se->avg.load_sum = divider;
        if (se_weight(se)) {
            /* 对于tse,其load_sum中权重没有参与计算,而cfs_rq的load_sum参数计算了 */
            se->avg.load_sum = div_u64(se->avg.load_avg * se->avg.load_sum, se_weight(se));
        }
    
        /*
         * 负载竟然是直接相加
         *
         * enqueue_load_avg 执行:
         * cfs_rq->avg.load_avg += se->avg.load_avg;
         * cfs_rq->avg.load_sum += se_weight(se) * se->avg.load_sum;
         */
        enqueue_load_avg(cfs_rq, se);
        cfs_rq->avg.util_avg += se->avg.util_avg;
        cfs_rq->avg.util_sum += se->avg.util_sum;
        cfs_rq->avg.runnable_avg += se->avg.runnable_avg;
        cfs_rq->avg.runnable_sum += se->avg.runnable_sum;
    
        /* 启动传播过程的是这个函数 */
        add_tg_cfs_propagate(cfs_rq, se->avg.load_sum);
        /* 触发调频 */
        cfs_rq_util_change(cfs_rq, 0);
        trace_pelt_cfs_tp(cfs_rq);
    }

    调用路径:

    switched_to_fair //从非CSF调度类变为CFS调度类
    task_move_group_fair //移到tg中来
        attach_task_cfs_rq
        post_init_entity_util_avg //新建的任务开始挂到cfs_rq上
            attach_entity_cfs_rq
            update_load_avg //迁移到当前CPU
                attach_entity_load_avg

    2. 当任务从当前CPU迁移走、从CFS调度类变为非CFS调度类,都会调用 detach_entity_load_avg() 来删除se对当前cfs_rq负载的影响。

    static void detach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se)
    {
        /*
         * dequeue_load_avg 执行:
         * sub_positive(&cfs_rq->avg.load_avg, se->avg.load_avg);
         * sub_positive(&cfs_rq->avg.load_sum, se_weight(se) * se->avg.load_sum);
         */
        dequeue_load_avg(cfs_rq, se);
        sub_positive(&cfs_rq->avg.util_avg, se->avg.util_avg);
        sub_positive(&cfs_rq->avg.util_sum, se->avg.util_sum);
        sub_positive(&cfs_rq->avg.runnable_avg, se->avg.runnable_avg);
        sub_positive(&cfs_rq->avg.runnable_sum, se->avg.runnable_sum);
    
        /* 不使能组调度时是空函数 */
        add_tg_cfs_propagate(cfs_rq, -se->avg.load_sum);
    
        cfs_rq_util_change(cfs_rq, 0);
    
        trace_pelt_cfs_tp(cfs_rq);
    }

    调用路径:

    set_task_cpu //任务从当前CPU迁移走
        migrate_task_rq_fair //还有一个唤醒迁移路径
    switched_from_fair //从CFS调度类变为非CFS调度类
    task_move_group_fair //从tg中移走
        detach_task_cfs_rq
            detach_entity_cfs_rq
                detach_entity_load_avg

    3. 唤醒选核选到new_cpu而导致的迁移,由于由于持rq锁,想缩短持锁临界区,负载的移除会做延迟处理,在删除时只是记录到 cfs_rq->removed 成员中,然后在下次更新cfs_rq的负载时再进行删除。

    //唤醒迁移中的记录:
    set_task_cpu //任务从当前CPU迁移走
        migrate_task_rq_fair //若是唤醒迁移路径中先记录再更新
            remove_entity_load_avg(&p->se);
    
    static void remove_entity_load_avg(struct sched_entity *se)
    {
        struct cfs_rq *cfs_rq = cfs_rq_of(se);
        unsigned long flags;
    
        sync_entity_load_avg(se);
    
        raw_spin_lock_irqsave(&cfs_rq->removed.lock, flags);
        /* 每次remove都会累加 */
        ++cfs_rq->removed.nr;
        cfs_rq->removed.util_avg    += se->avg.util_avg;
        cfs_rq->removed.load_avg    += se->avg.load_avg;
        cfs_rq->removed.runnable_avg    += se->avg.runnable_avg;
        raw_spin_unlock_irqrestore(&cfs_rq->removed.lock, flags);
    }

    删除函数调用路径为: update_load_avg --> update_cfs_rq_load_avg()

    static inline int update_cfs_rq_load_avg(u64 now, struct cfs_rq *cfs_rq)
    {
        unsigned long removed_load = 0, removed_util = 0, removed_runnable = 0;
        struct sched_avg *sa = &cfs_rq->avg;
        int decayed = 0;
    
        if (cfs_rq->removed.nr) {
            unsigned long r;
            u32 divider = get_pelt_divider(&cfs_rq->avg);
    
            raw_spin_lock(&cfs_rq->removed.lock);
            /* 值交换,将原removed的成员清0 */
            swap(cfs_rq->removed.util_avg, removed_util);
            swap(cfs_rq->removed.load_avg, removed_load);
            swap(cfs_rq->removed.runnable_avg, removed_runnable);
            cfs_rq->removed.nr = 0;
            raw_spin_unlock(&cfs_rq->removed.lock);
    
            /* 对于cfs_rq load_avg=load_sum/divider */
            r = removed_load;
            sub_positive(&sa->load_avg, r);
            sub_positive(&sa->load_sum, r * divider);
    
            /* cfs_rq的 util_avg 也等于 util_sum/divider, 和task se是保持一致的 */
            r = removed_util;
            sub_positive(&sa->util_avg, r);
            sub_positive(&sa->util_sum, r * divider);
            sa->util_sum = max_t(u32, sa->util_sum, sa->util_avg * PELT_MIN_DIVIDER);
    
            r = removed_runnable;
            sub_positive(&sa->runnable_avg, r);
            sub_positive(&sa->runnable_sum, r * divider);
    
            /* 若不使能组调度就是空函数 */
            add_tg_cfs_propagate(cfs_rq, -(long)(removed_runnable * divider) >> SCHED_CAPACITY_SHIFT);
    
            decayed = 1;
        }
    
        decayed |= __update_load_avg_cfs_rq(now, cfs_rq);
    
        return decayed;
    }

    注意:PELT算法中任务休眠的时候并没有将其移除,只是会不断的衰减。 

    七、调试接口

    1. /proc/pid/sched

    此文件列出task se的负载,有*_sum 和*_avg。

    2. /proc/sched_debug

    此文件默认只列出每个cfs_rq的*_avg,可以自己添加对*_sum的打印。

    3. early_param pelt

    可以在设备树中配置选择PELT衰减50%需要的周期数,通过 /sys/firmware/devicetree/base/chosen/bootargs 和 /proc/cmdline 进行确认配置。

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

    4. trace_pelt_se_tp 等hook接口

    trace_pelt_{se/cfs/rt/dl/thermal/irq/}_tp 这些tarce hook在每次更新各类负载时都会调用,但没有在 tracefs 中导出,仅提供挂钩机制用于测试和调试目的。后缀为 _tp 以使它们在代码中易于识别。

    八、相关实验

    1. 死循环绑核实验

    # let i=0; while true; do let i=i+1; done &
    [1] 23575
    # let i=0; while true; do let i=i+1; done &
    [2] 23576
    # let i=0; while true; do let i=i+1; done &
    [3] 23577
    
    # renice -n -20 -p 23575
    # renice -n -10 -p 23576
    
    # taskset -p 80 23575
    # taskset -p 80 23576
    # taskset -p 80 23577
    
    # cat /proc/23575/sched
    se.load.weight            :  90891264 // 88761*1024=90891264 scale up 后的weight
    se.avg.load_sum           :     11769 // 趋近于 LOAD_AVG_MAX=12336, pelt=8
    se.avg.runnable_sum       :  12052801 // 趋近于 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE=12336*1024=12632064 误差4.6%
    se.avg.util_sum           :   9797450 // 只计running,非独占CPU,优先级越高运行越多越接近 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE
    se.avg.load_avg           :     88745 // 恒runnable趋近于其权重88761,误差0.018%
    se.avg.runnable_avg       :      1023 // 趋近于1024, runnable% * SCHED_CAPACITY_SCALE
    se.avg.util_avg           :       832 // 只计running,非独占CPU,优先级越大越接近1024, %running * SCHED_CAPACITY_SCALE
    
    # cat /proc/23576/sched
    se.load.weight            :   9777152 // 9548*1024=9777152
    se.avg.load_sum           :     12127 // 趋近于 LOAD_AVG_MAX=12336, pelt=8
    se.avg.runnable_sum       :  12420077 // 趋近于 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE=12336*1024=12632064 误差1.67%
    se.avg.util_sum           :   2945713
    se.avg.load_avg           :      9546 // 恒runnable趋近于其权重9548
    se.avg.runnable_avg       :      1023 // 趋近于1024, runnable% * SCHED_CAPACITY_SCALE
    se.avg.util_avg           :       242
    
    # cat /proc/23577/sched
    se.load.weight            :   1048576 // 1024*1024=1048576
    se.avg.load_sum           :     12096 // 趋近于 LOAD_AVG_MAX=12336, pelt=8
    se.avg.runnable_sum       :  12386407 // 趋近于 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE=12336*1024=12632064 误差1.94%
    se.avg.util_sum           :   2720768
    se.avg.load_avg           :      1024 // 恒runnable趋近于其权重1024
    se.avg.runnable_avg       :      1024 // 趋近于1024, runnable% * SCHED_CAPACITY_SCALE
    se.avg.util_avg           :       224
    
    # cat /proc/sched_debug
    cfs_rq[7]:/
      .nr_running     : 3
      .h_nr_running   : 3
      .load           : 101716992  // (88761+9548+1024)*1024=101716992 权重之和再scale up.
      .load_sum       : 1167041828 // (88761+9548+1024)*12336=1225371888 误差4.77%,公式 Sum(se::weight)*LOAD_AVG_MAX
      .runnable_sum   : 36092258   // 3*12336*1024=37896192 误差4.76%,公式 nr_running*LOAD_AVG_MAX*SCHED_CAPACITY_SCALE
      .util_sum       : 12030751   // 12336*1024=12632064 误差4.76%,公式 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE
      .load_avg       : 99331      // 88761+9548+1024=99333, 一个恒运行的cfs_rq,趋近其权重。
      .runnable_avg   : 3071       // 3*1024=3072, 公式 nr_running*SCHED_CAPACITY_SCALE
      .util_avg       : 1023        // 1024, SCHED_CAPACITY_SCALE

    小结:
    (1) 对于 load_avg 来说,task se 和 cfs_rq 是一样的,若恒运行都趋近其权重,但是二者权重不同,因此值不同。
    (2) 对于 runnable_avg 来说,考虑了cfs_rq上任务个数,cfs_rq和task se是一个倍数关系。
    (3) 对于 util_avg 来说,task se 和 cfs_rq 是一样的,都是 running%*SCHED_CAPACITY_SCALE,一个恒运行的任务和一个跑满的cfs_rq的util_avg是一样的。
    (4) 对于 task se,load_sum 的最大值是 LOAD_AVG_MAX,runnable_sum 的最大值是 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE,util_sum 的最大值是 LOAD_AVG_MAX*SCHED_CAPACITY_SCALE,load_avg 的最大值是其 sched_prio_to_weight[] 对应的权重,runnable_avg 最大值是 SCHED_CAPACITY_SCALE,util_avg 最大值是 SCHED_CAPACITY_SCALE。
    (5) 对于 cfs_rq,load_sum 的最大值是cfs_rq的权重Sum(se::weight)*LOAD_AVG_MAX,runnable_sum 的最大值是 nr_running*LOAD_AVG_MAX*SCHED_CAPACITY_SCALE,util_sum 的最大值是 LOAD_AVG_MAX * SCHED_CAPACITY_SCALE,load_avg 的最大值是其权重Sum(se::weight),runnable_avg 最大值是 nr_running*SCHED_CAPACITY_SCALE,util_avg 最大值是 SCHED_CAPACITY_SCALE 。

    不同的优先级之间负载之间好像并没有直接明确的关系!

    九、总结

    1. struct sched_avg 注释中 *_avg 的定义

    task se load_avg 的定义:load_avg = runnable% * scale_load_down(load)
    task se runnable_avg 定义: runnable_avg = runnable% * SCHED_CAPACITY_SCALE
    task se util_avg 定义: util_avg = running% * SCHED_CAPACITY_SCALE

    2. 对于"*_avg"、"*_sum" 的统计方法可在本文中检索"*:"。

    参考:

    PELT算法浅析:http://www.wowotech.net/process_management/pelt.html

     kernel\Documentation\scheduler\sched-pelt.c

  • 相关阅读:
    【PHP】 lumen 输出sql信息
    Go学习笔记-使用MySQL数据库
    PHP检测函数是否存在
    Javascript边框闪烁提示
    【转】Ubuntu 16.04下 Mysql 5.7.17源码编译与安装
    python-mysql windows diver地址
    【转载】Python Flask 开发环境搭建(Windows)
    【转载】agentzh 的 Nginx 教程(版本 2016.07.21)
    【转载】写给新手看的Flask+uwsgi+Nginx+Ubuntu部署教程
    【转载】从零开始搭建论坛(三):Flask框架简单介绍
  • 原文地址:https://www.cnblogs.com/hellokitty2/p/16735251.html
Copyright © 2020-2023  润新知