基于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_avg、runnable_avg、util_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