EEVDF代码详解(一)

前言

EEVDF是由Peter Zijlstra提出并在kernel-6.6中合入主线的调度算法,已经替代CFS成为Linux kernel中普通进程的默认调度器。基本介绍可参考CFS的完美进化版 – EEVDF调度器简介,本文针对EEVDF中的一些关键代码进行讲解。

1. 如何挑选下一个进程

eevdf挑选进程的核心理念是:

  1. 选择合格的进程(也就是lag >= 0)
  2. 在合格的进程中选择虚拟截至时间最早的

deadline也用红黑树来保存,且维护了每个se的min_deadline,以保证可以更快找到最小虚拟截至时间
se->min_deadline = min(se->deadline, se->{left,right}->min_deadline)
这里是利用了最小堆的思想

代码实现

现在pick_next_entity的逻辑很清晰,主要在pick_eevdf中

static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	/*
	 * Enabling NEXT_BUDDY will affect latency but not fairness.
	 */
	if (sched_feat(NEXT_BUDDY) &&
	    cfs_rq->next && entity_eligible(cfs_rq, cfs_rq->next))
		return cfs_rq->next;

	return pick_eevdf(cfs_rq);
}
pick_eevdf
static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
{
	struct sched_entity *se = __pick_eevdf(cfs_rq);

	if (!se) {
		struct sched_entity *left = __pick_first_entity(cfs_rq);
		if (left) {
			pr_err("EEVDF scheduling fail, picking leftmost\n");
			return left;
		}
	}

	return se;
}
__pick_eevdf

主要就是两个步骤:

  1. 找到包含min_deadline的那个分支best_left
  2. 从best_left中找到包含min_deadline的调度实体
static struct sched_entity *__pick_eevdf(struct cfs_rq *cfs_rq)
{
	struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
	struct sched_entity *curr = cfs_rq->curr;
	struct sched_entity *best = NULL;
	struct sched_entity *best_left = NULL;

	if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr)))
		curr = NULL;
	best = curr;

	/*
	 * Once selected, run a task until it either becomes non-eligible or
	 * until it gets a new slice. See the HACK in set_next_entity().
	 */
	if (sched_feat(RUN_TO_PARITY) && curr && curr->vlag == curr->deadline)
		return curr;

	while (node) {
		struct sched_entity *se = __node_2_se(node);

		/*
		 * 如果当前se不合格,就直接找他的左子树.
		 */
		if (!entity_eligible(cfs_rq, se)) {
			node = node->rb_left;
			continue;
		}

		/*
		 * 如果best为null或者se的deadline更小,那就更新best
		 */
		if (!best || deadline_gt(deadline, best, se))
			best = se;

		/*
		 * Every se in a left branch is eligible, keep track of the
		 * branch with the best min_deadline
		 */
		if (node->rb_left) {
			struct sched_entity *left = __node_2_se(node->rb_left);

			/*
			 *best_left保存包含min_deadline的分支,所以如果当前节点的左子树的min_deadline更小,
			 *那就更新best_left
			 */
			if (!best_left || deadline_gt(min_deadline, best_left, left))
				best_left = left;

			/*
			 * 如果当前se的min_deadline已经等于左子树中的min_deadline,说明已经找到
			 * 最小的min_deadline了,直接退出。
			 */
			if (left->min_deadline == se->min_deadline)
				break;
		}

		/* 如果当前se的deadline等于min_deadline,就不用再去找右子树了 */
		if (se->deadline == se->min_deadline)
			break;

		/* 否则,min_deadline在右子树中, */
		node = node->rb_right;
	}

	/*
	 * We ran into an eligible node which is itself the best.
	 * (Or nr_running == 0 and both are NULL)
	 */
	if (!best_left || (s64)(best_left->min_deadline - best->deadline) > 0)
		return best;

	/*
	 * 现在我们已经找到存在min_deadline的best_left分支了,且该分支中所有的进程都是合格的,
	 * 只需要在此分支中找到deadline = min_deadline的se就可以了。
	 */
	node = &best_left->run_node;
	while (node) {
		struct sched_entity *se = __node_2_se(node);

		/* min_deadline is the current node */
		if (se->deadline == se->min_deadline)
			return se;

		/* min_deadline is in the left branch */
		if (node->rb_left &&
		    __node_2_se(node->rb_left)->min_deadline == se->min_deadline) {
			node = node->rb_left;
			continue;
		}

		/* else min_deadline is in the right branch */
		node = node->rb_right;
	}
	return NULL;
}

2. 任务放置

(1). 如何设定lag值?

如果想要放置一个task,且保持一定的滞后量,应该如何设定lag值?由于将一个task放入队列后会对整体的weight造成影响,从而影响平均虚拟时间V,那么放入队列之后实际的滞后量一定和我们最初设定的lag值有偏差,我们需要提前计算出这部分偏差,以便设定和我们预期效果一样的lag值。

(2). 公式推导

我们最终需要的是weight对lag的影响,所以最终应该得到加入队列之后的lag: l_i’与加入队列前设定的l_i的比例关系,这个值应该受队列当前总权重W和该进程w_i的影响。

进程i的lag值表示为l_i,平均虚拟时间为V,进程i的虚拟运行时间为v_i,则有:

l_i = V - v_i ⇒ v_i = V - l_i

进程入队之后的平均虚拟运行时间V’可以表示为:

//W和V分别为进程入队前的权重和平均虚拟运行时间
V' = (W * V + w_i * v_i) / (W + w_i)  
   = (W * V + w_i * (V - l_i)) / (W + w_i)
   = (V * (W + w_i) - w_i * l_i) / (W + w_i)
   = V - w_i * l_i / (W + w_i)

进程入队之后的l_i’可以表示为:

l_i' = V' - v_i
     = V - w_i * l_i / (W + w_i) - (V - l_i)
     = l_i - w_i * l_i / (W + w_i)
     = l_i * W / (W + w_i)

可以看到进程入队之后的l_i’是严格小于入队前设定的l_i的,入队前后的lag和入队前后的总权重呈反比,也就是:

l_i'/l_i = W/W' //W’为入队后的总权重

所以我们可以在入队前设定lag时先乘(W+w_i)/W,就可以让其滞后值与我们预期保持一致了。

(3). 代码实现

理解了上述公式推导,再看代码就很简单了

static void
place_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	u64 vslice, vruntime = avg_vruntime(cfs_rq);
	s64 lag = 0;
	//根据weight计算应得的虚拟时间片
	se->slice = sysctl_sched_base_slice;
	vslice = calc_delta_fair(se->slice, se);

	/*
	 * Due to how V is constructed as the weighted average of entities,
	 * adding tasks with positive lag, or removing tasks with negative lag
	 * will move 'time' backwards, this can screw around with the lag of
	 * other tasks.
	 *
	 * EEVDF: placement strategy #1 / #2
	 */
	if (sched_feat(PLACE_LAG) && cfs_rq->nr_running) {
		struct sched_entity *curr = cfs_rq->curr;
		unsigned long load;

		lag = se->vlag;
		
		load = cfs_rq->avg_load;
		if (curr && curr->on_rq)
			load += scale_load_down(curr->load.weight);

		//计算l_i * (W + w_i)
		lag *= load + scale_load_down(se->load.weight);
		if (WARN_ON_ONCE(!load))
			load = 1;
		//计算 l_i * (W + w_i) / W
		lag = div_s64(lag, load);
	}
	
	//设定初始虚拟运行时间为:当前平均虚拟时间 - 滞后量: v_i = V - l_i
	se->vruntime = vruntime - lag;

	/*
	 * When joining the competition; the exisiting tasks will be,
	 * on average, halfway through their slice, as such start tasks
	 * off with half a slice to ease into the competition.
	 */
	 /*给新进程的虚拟时间片/2,让其更早一点运行
	  *因为将当前进程加入队列时,当前队列中的进程大概平均消耗掉了他们一半的时间片,
	  *所以将新进程的时间片也/2,保证其略等于平均值,能及时得到运行。
	  */
	if (sched_feat(PLACE_DEADLINE_INITIAL) && (flags & ENQUEUE_INITIAL))
		vslice /= 2;

	/*
	 * EEVDF: vd_i = ve_i + r_i/w_i
	 */
	se->deadline = se->vruntime + vslice;
}

3. 如何更新deadline

只有当当前时间片已经用完,也就是vruntime > deadline时才会去更新deadline。
deadline = vruntime + vslice
其中vslice是申请的虚拟时间片,与权重有关
vslice = r_i / w_i
r_i是实际申请的时间片,可以看出weight越大,vslice越小,deadline越小,也就更容易优先运行。

这里和vruntime的原理是一样的,vruntime同样也是相同物理时间时,weight越大,vruntime越小

代码实现

static void update_deadline(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	/*
	 *如果当前vruntime还小于deadline,那说明时间片还没有用完,不需要更新
	 */
	if ((s64)(se->vruntime - se->deadline) < 0)
		return;

	/*
	 * For EEVDF the virtual time slope is determined by w_i (iow.
	 * nice) while the request time r_i is determined by
	 * sysctl_sched_base_slice.
	 */
	se->slice = sysctl_sched_base_slice;

	/*
	 *deadline等于当前vruntime加上申请的虚拟时间片(虚拟时间片和负载w_i相关)
	 * EEVDF: vd_i = ve_i + r_i / w_i
	 */
	se->deadline = se->vruntime + calc_delta_fair(se->slice, se);

	/*
	 * 当前进程申请的时间片已经用完了,将其标记为可抢占
	 */
	if (cfs_rq->nr_running > 1) {
		resched_curr(rq_of(cfs_rq));
		clear_buddies(cfs_rq, se);
	}
}

参考

  1. [PATCH 06/17] sched/fair: Add lag based placement
  2. [PATCH 08/17] sched/fair: Implement an EEVDF like policy
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值