Kernel源码笔记之调度:1.PELT

Kernel源码笔记目录

PELT 算法

时间被分成了1024us的序列(也就是1ms,按1024算比较好算),在每一个1024us的周期中,一个entity对系统负载的贡献可以根据该实体处于runnable状态(正在CPU上运行或者等待cpu调度运行)的时间进行计算。任务在1024us的周期窗口内的负载其实就是瞬时负载。如果在该周期内,runnable的时间是t,那么该任务的瞬时负载应该和(t/1024)有正比的关系。类似的概念,任务的瞬时利用率应该通过1024us的周期窗口内的执行时间(不包括runqueue上的等待时间)比率来计算。

瞬时负载:Li = load weight x (t/1024),瞬时负载中引用的权重。

Ui = Max CPU capacity x (t/1024)。瞬时利用率中Max CPU capacity最大为1024。

任务的平均负载:
L = L0 + L1y + L2y2 + L3*y3 + …

Li表示在周期pi中的瞬时负载,对于过去的负载我们在计算的时候需要乘一个衰减因子y。在目前的内核代码中,y是确定值:y ^32等于0.5。这样选定的y值,一个调度实体的负荷贡献经过32个窗口(1024us)后,对当前时间的的符合贡献值会衰减一半。

下一层的cfs rq的h_load = 上一层cfs rq的h_load X group se 在上一层cfs负载中的占比。

Task se的h_load = task se的load avg x cfs rq的h_load / cfs rq的load avg

struct sched_avg {
    // 上次更新的时间
	u64				last_update_time; 

    /**
    *_sum是按几何级数的累加,按照1毫秒一个周期,距离当前点越远,
    衰减越厉害,32个周期后load衰减50%
    */

    // runnable+running的时间
	u64				load_sum;

    // runnable 的时间
	u64				runnable_load_sum;

    // running的时间
	u32				util_sum;

    // 在更新负载时分三段,
    // d1(合入上次更新负载的剩余时间,即不足1ms窗口的时间)
    // d2(满窗时间)
    // d3(不足1ms的时间)
    // period_contrib则记录了d3窗口的时间,方便下次计算d1的时间
	u32				period_contrib;

    // 负载平均值
	unsigned long			load_avg;

    // runnable时的负载平均值
	unsigned long			runnable_load_avg;

    // running时的负载平均值
	unsigned long			util_avg;

    /**
    任务阻塞后,其负载会不断衰减。如果一个重载任务阻塞太长时间,
    那么根据PELT算法计算出来的负载会非常小,当该任务被唤醒重新
    参与调度的时候,由于负载较小会让调度器做出错误的判断。因此
    引入了这个成员,记录阻塞之前的load_avg信息
    */
	struct util_est			util_est;
} ____cacheline_aligned;

load_avg的初始化有2个地方:

  • init_entity_runnable_average,这个在task fork的时候,或者在cgroup创建的时候。
  • post_init_entity_util_avg,在这个阶段里只更新util_avg,这个只有task才会走,因为这个是在wake_up_new_task的时候调用的。
void init_entity_runnable_average(struct sched_entity *se)
{
	struct sched_avg *sa = &se->avg;

	memset(sa, 0, sizeof(*sa));

	/*
     * 原文注释:
	 * 任务被初始化为满负载,被视为繁重的任务,直到它们有机会稳定到其实际负载水平。 
     * 组实体初始化为零负载以反映尚未将任何内容附加到任务组的事实。
	 */
	if (entity_is_task(se))
        // scale_load_down是将权重按比例向下缩小(右移10位)。只在64位平台上有用,在32位平台上,还是
        // 返回原值。
        // 如果在64位平台上,nice0的权重为1024,经过scale_load_down后,权重归为1
        // 所以这里将task的运行平均负载和总平均负载都初始化成1
		sa->runnable_load_avg = sa->load_avg = scale_load_down(se->load.weight);

    // 初始化运行权重
	se->runnable_weight = se->load.weight;

	/* when this task enqueue'ed, it will contribute to its cfs_rq's load_avg */
}

# define SCHED_FIXEDPOINT_SHIFT		10

#ifdef CONFIG_64BIT
# define NICE_0_LOAD_SHIFT	(SCHED_FIXEDPOINT_SHIFT + SCHED_FIXEDPOINT_SHIFT)
# define scale_load(w)		((w) << SCHED_FIXEDPOINT_SHIFT)
# define scale_load_down(w) \
({ \
	unsigned long __w = (w); \
	if (__w) \
		__w = max(2UL, __w >> SCHED_FIXEDPOINT_SHIFT); \
	__w; \
})
#else
# define NICE_0_LOAD_SHIFT	(SCHED_FIXEDPOINT_SHIFT)
# define scale_load(w)		(w)
# define scale_load_down(w)	(w)
#endif

void post_init_entity_util_avg(struct sched_entity *se)
{
	struct cfs_rq *cfs_rq = cfs_rq_of(se);
	struct sched_avg *sa = &se->avg;

    // 传NULL进去,最后算出来的值是1024
	long cpu_scale = arch_scale_cpu_capacity(NULL, cpu_of(rq_of(cfs_rq)));

    // avg.util_avg在初始化时为0,所以cap算出来是512
	long cap = (long)(cpu_scale - cfs_rq->avg.util_avg) / 2;

	if (cap > 0) {

		if (cfs_rq->avg.util_avg != 0) {
            // 队列里面还有别的任务

            // todo: 这个计算公式没看懂
			sa->util_avg  = cfs_rq->avg.util_avg * se->load.weight;
			sa->util_avg /= (cfs_rq->avg.load_avg + 1);

			if (sa->util_avg > cap)
				sa->util_avg = cap;
		} else {
            // 队列里还没有任务时,将util_avg初始化为队列的算力
			sa->util_avg = cap;
		}
	}

	if (entity_is_task(se)) {
		struct task_struct *p = task_of(se);
		if (p->sched_class != &fair_sched_class) {
			// 如果不是公平调度,则设置上次更新时间为当前cpu时间
			se->avg.last_update_time = cfs_rq_clock_task(cfs_rq);
			return;
		}
	}

    // 将se添加到当前队列
	attach_entity_cfs_rq(se);
}

static void attach_entity_cfs_rq(struct sched_entity *se)
{
	struct cfs_rq *cfs_rq = cfs_rq_of(se);

#ifdef CONFIG_FAIR_GROUP_SCHED
	// 如果是组公平,则计算层级深度
	se->depth = se->parent ? se->parent->depth + 1 : 0;
#endif

    // 更新task和它所在队列的平均负载
    // sched_feat是有无cpu特征, 在kernel/sched/feature.h中做了初始化
    // ATTACH_AGE_LOAD这个特征默认为true,所以传给update_load_avg的为0
	update_load_avg(cfs_rq, se, sched_feat(ATTACH_AGE_LOAD) ? 0 : SKIP_AGE_LOAD);

	// 将当前这个se加到运行队列里
	attach_entity_load_avg(cfs_rq, se, 0);

	// 下面2个函数和组调度有关
	update_tg_load_avg(cfs_rq, false);
	propagate_entity_cfs_rq(se);
}

/*
 * Optional action to be done while updating the load average
 */
#define UPDATE_TG	0x1
#define SKIP_AGE_LOAD	0x2
#define DO_ATTACH	0x4

/* Update task and its cfs_rq load average */
static inline void update_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	u64 now = cfs_rq_clock_task(cfs_rq);
	struct rq *rq = rq_of(cfs_rq);
	int cpu = cpu_of(rq);

    // 负载是否衰减
	int decayed;

	// 当last_update_time不为0时,则更新se的负载数据
	if (se->avg.last_update_time && !(flags & SKIP_AGE_LOAD))
		__update_load_avg_se(now, cpu, cfs_rq, se);

    // 更新完se后,更新队列平均负载
	decayed  = update_cfs_rq_load_avg(now, cfs_rq);

    // 向上传播平均负载,主要是组调度里的负载更新
	decayed |= propagate_entity_load_avg(se);

    // flags为0,下面都不走
	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.
		 */
		attach_entity_load_avg(cfs_rq, se, SCHED_CPUFREQ_MIGRATION);
		update_tg_load_avg(cfs_rq, 0);

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

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_sum = 0;
	struct sched_avg *sa = &cfs_rq->avg;
	int decayed = 0;
    
    // 当有移除的task时走这个分支
	if (cfs_rq->removed.nr) {
		unsigned long r;
		u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib;

		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);
		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);

		decayed = 1;
	}

    // 更新队列平均负载
	decayed |= __update_load_avg_cfs_rq(now, cpu_of(rq_of(cfs_rq)), cfs_rq);

#ifndef CONFIG_64BIT
    // smp_wmb好像是插入一个内存屏障
	smp_wmb();

    // 更新负载的时间改变
	cfs_rq->load_last_update_time_copy = sa->last_update_time;
#endif

    // 如果负载有衰减,则更新队列的util值
	if (decayed)
		cfs_rq_util_change(cfs_rq, 0);

	return decayed;
}

int __update_load_avg_cfs_rq(u64 now, int cpu, struct cfs_rq *cfs_rq)
{
    // load.weight和runnable_weight都被缩小2的10次方(右移10位)
	if (___update_load_sum(now, cpu, &cfs_rq->avg,
				scale_load_down(cfs_rq->load.weight),
				scale_load_down(cfs_rq->runnable_weight),
				cfs_rq->curr != NULL)) {

		___update_load_avg(&cfs_rq->avg, 1, 1);
		return 1;
	}

	return 0;
}

/**
now: 当前时间,这个时间是针对cpu调整过的, 单位是ns
cpu: 当前cpu
sa: 队列struct sched_avg结构体
load: 队列的负载权重
runnable: 队列的可运行负载权重
running: 当前队列是否正在运行,这个运行仅判断cfs_rq->curr不为0 

这个函数返回 0 表示没有更新负载和
*/
static __always_inline int
___update_load_sum(u64 now, int cpu, struct sched_avg *sa,
		  unsigned long load, unsigned long runnable, int running)
{
	u64 delta;

	// 计算经上次更新时间后的间隔
	// last_update_time的单位是ns
	delta = now - sa->last_update_time;
	/*
	 原文注释:
	 这应该只在时间倒退时发生,不幸的是,当我们切换到 TSC 时,它会在 sched clock init 期间发生。
	 */
	if ((s64)delta < 0) {
		// 如果确实出现了这种情况,则重置上次更新的时间
		sa->last_update_time = now;
		return 0;
	}

	/*
	 原文注释:
	 使用 1024ns 作为测量单位,因为它是 1us 的合理近似值并且计算速度很快。
	 */
	// 使用1024直接可以用移位进行计算,所以速度快,这里向右移10位,就算出了
	// 有多少个1us,
	delta >>= 10;
	if (!delta)
		return 0;

	// 这里又将delta转化成ns后,加到last_update_time,
	// 这里为什么不直接等于now呢?我猜应该是last_update_time只记录整数us数,
	// 在前面已经将delta归整为纳秒数(虽然是以1024为单位,但是不要在间这些细节)
	sa->last_update_time += delta << 10;

	/*
	 * running is a subset of runnable (weight) so running can't be set if
	 * runnable is clear. But there are some corner cases where the current
	 * se has been already dequeued but cfs_rq->curr still points to it.
	 * This means that weight will be 0 but not running for a sched_entity
	 * but also for a cfs_rq if the latter becomes idle. As an example,
	 * this happens during idle_balance() which calls
	 * update_blocked_averages()
	 */
	 // 当负载为0时,重置runnable和running为0
	 // todo: 这个条件没看懂
	if (!load)
		runnable = running = 0;

	// 计算累积的负载值
	if (!accumulate_sum(delta, cpu, 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, int cpu, struct sched_avg *sa,
	       unsigned long load, unsigned long runnable, int running)
{
	unsigned long scale_freq, scale_cpu;
	u32 contrib = (u32)delta; /* p == 0 -> delta < 1024 */
	u64 periods;

	// scale_freq在x86平台是1024,这个应该是cpu的频率
	scale_freq = arch_scale_freq_capacity(cpu);

	// 这里的scale_cpu算出来也是1024,因为sd传的是NULL
	scale_cpu = arch_scale_cpu_capacity(NULL, cpu);

	// period_contrib保存的是上一次计算时d3的值,这里将d3的
	// 值再加上,则好和d1组成了一个完整的周期
	delta += sa->period_contrib;

	// 注释说的很清楚,周期是以1毫秒为单位的
	periods = delta / 1024; /* A period is 1024us (~1ms) */

	
	if (periods) {

		// 第一步:先衰减老的统计数据
		sa->load_sum = decay_load(sa->load_sum, periods);
		sa->runnable_load_sum =
			decay_load(sa->runnable_load_sum, periods);
		sa->util_sum = decay_load((u64)(sa->util_sum), periods);

		// delta现在是新的d3的值
		delta %= 1024; 

		// 1024-sa->period_contrib 就是 d1的值
		// __accumulate_pelt_segments是计算本次的d1,d2,d3的值
		contrib = __accumulate_pelt_segments(periods,
				1024 - sa->period_contrib, delta);
	}

	// 保存d3的值
	sa->period_contrib = delta;

	// cap_scale展开:contrib = (contrib * scale_freq) >> 10
	// 在x86平台上,经过计算后的contrib不变
	contrib = cap_scale(contrib, scale_freq);

	// 在进程中load和runnable的权重是一样的,都被变换为1024的倍数
	// 所以在这里累加负载和的时候,将进行的权重也加到里面
	// 权重大的进程负载也重
	if (load)
		sa->load_sum += load * contrib;
	if (runnable)
		sa->runnable_load_sum += runnable * contrib;

	// 计算运行中负载时,将cpu的算力也加了进去
	if (running)
		sa->util_sum += contrib * scale_cpu;

	return periods;
}

/*
 * Approximate:
 *   val * y^n,    where y^32 ~= 0.5 (~1 scheduling period)
 * y^32是0.5,经过32个周期,负载缩减一半
 */
static u64 decay_load(u64 val, u64 n)
{
	unsigned int local_n;

	// LOAD_AVG_PERIOD 是32,经过32*63个周期,负载缩减为0
	if (unlikely(n > LOAD_AVG_PERIOD * 63))
		return 0;

	/* after bounds checking we can collapse to 32-bit */
	local_n = n;

	// 先计算整32数倍周期的,因为32个周期缩减一半,
	// 直接用移位就可以计算,效率高,
	// 估计这也是为什么把负载周期设为1024的原因
	if (unlikely(local_n >= LOAD_AVG_PERIOD)) {
		val >>= local_n / LOAD_AVG_PERIOD;

		// local_n是剩余的小于32的周期
		local_n %= LOAD_AVG_PERIOD;
	}

	// runnable_avg_yN_inv是32个周期的速算数
	// mul_u64_u32_shr实际上就是计算 val * runnable_avg_yN_inv[local_n]
	// mul_u64_u32_shr是做64位与32位的乘法
	val = mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32);
	return val;
}

static u32 __accumulate_pelt_segments(u64 periods, u32 d1, u32 d3)
{
	// c3也就是d3
	u32 c1, c2, c3 = d3; /* y^0 == 1 */

	/*
	 * c1 = d1 y^p
	 */
	 // 先将d1的值衰减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的计算变换了一下,加快计算效率
	c2 = LOAD_AVG_MAX - decay_load(LOAD_AVG_MAX, periods) - 1024;

	return c1 + c2 + c3;
}
static __always_inline void
___update_load_avg(struct sched_avg *sa, unsigned long load, unsigned long runnable)
{
	// todo: divider的计算没看懂
	u32 divider = LOAD_AVG_MAX - 1024 + sa->period_contrib;

	// div_u64是计算除法的:第一个参数 / 第二个参数
	// todo: 这里为什么要乘以load和runnable没看懂
	sa->load_avg = div_u64(load * sa->load_sum, divider);
	sa->runnable_load_avg =	div_u64(runnable * sa->runnable_load_sum, divider);

	// 对于64位及64位以下的类型,WRITE_ONCE展开后就是: 第一个参数 = 第二个参数
	// 大于64位的类型,WRITE_ONCE展开后就是memcpy(第一个参数,第二个参数, sizeof(第一个参数))
	WRITE_ONCE(sa->util_avg, sa->util_sum / divider);
}
static void attach_entity_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	u32 divider = LOAD_AVG_MAX - 1024 + cfs_rq->avg.period_contrib;

	// 同步队列的衰减窗口
	// 原文注释上说,如果不同步这个时间窗口,会出现一些奇怪的事情
	se->avg.last_update_time = cfs_rq->avg.last_update_time;
	se->avg.period_contrib = cfs_rq->avg.period_contrib;

	// 同步了衰减窗口之后,要再重新计算运行总值
	se->avg.util_sum = se->avg.util_avg * divider;

	// 总负载就是divider
	// todo: 没看懂
	se->avg.load_sum = divider;

	// se_weight是将se的权重按比例缩小(向右移10位)
	// 只有优先级比0大时才会进入这个分支,因为nice0的权重是1024,右移10位刚好是1.
	if (se_weight(se)) {
		// 重新计算总负载
		se->avg.load_sum =
			div_u64(se->avg.load_avg * se->avg.load_sum, se_weight(se));
	}

	// 先将可运行负载等于总负载
	se->avg.runnable_load_sum = se->avg.load_sum;

	// 将se的平均负载和总负载加到队列的相应负载上
	enqueue_load_avg(cfs_rq, se);

	// 将se运行时的平均负载和和总负载加到队列相应的变量上
	cfs_rq->avg.util_avg += se->avg.util_avg;
	cfs_rq->avg.util_sum += se->avg.util_sum;

	// 下面两个函数和组调度有关
	add_tg_cfs_propagate(cfs_rq, se->avg.load_sum);

	cfs_rq_util_change(cfs_rq, flags);
}

static inline void
enqueue_load_avg(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	cfs_rq->avg.load_avg += se->avg.load_avg;

	// 加总负载的要扩大se权重的倍数
	cfs_rq->avg.load_sum += se_weight(se) * se->avg.load_sum;
}

int __update_load_avg_se(u64 now, int cpu, struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	if (entity_is_task(se))
		// 如果是task,则运行时权重和nice的权重相同
		se->runnable_weight = se->load.weight;

	// ___update_load_sum的后三个参数是load(是否有负载), runnable(是否可运行), running(是否正在运行)
	// 对于普通进程来说,load和runnable传的值是相同的,都是!!se->on_rq(!!的意思是将值归为0和1),
	// se->on_rq表示是否在运行队列上。
	// 第三个参数传的是cfs_rq->curr == se, 表示当前进程是否正在运行
	if (___update_load_sum(now, cpu, &se->avg, !!se->on_rq, !!se->on_rq,
				cfs_rq->curr == se)) {
		// 更新平均负载
		// se_weight和se_runnable会将权重值缩小为nice0权重的倍数
		___update_load_avg(&se->avg, se_weight(se), se_runnable(se));

		// 好像是记录之前的负载
		// todo:没看懂。
		cfs_se_util_change(&se->avg);
		return 1;
	}

	return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值