CFS的覆灭,Linux新调度器EEVDF详解

本文主要总结了EEVDF论文和Linux内核实现中关键逻辑的推导,着重强调代码逻辑和论文公式之间的关系,它又长又全,像今天的汤圆又大又圆:D

Warn:多行的公式编号渲染有点问题,当存在多行公式时,仅对最后一条式子编号。

简 介

CFS 调度器强调公平,只提供权重一个变量(nice)用于控制任务之间运行时间分配的比例,不提供对特定任务时延层面的保障。这导致服务质量保障(Quality of Service,QoS)很难实现,传统手段是通过为不同任务设置标签,在调度关键逻辑(唤醒、抢占、负载均衡)对打了不同标签的任务进行特判处理,导致调度逻辑充满了特判和兜底。

为了将时延考虑在内,新调度器 EEVDF 在保障时间分配公平的同时,引入代表任务“应得时间”和“已得时间”的差异值 lag 作为关键指标,只有“应得时间”大于等于“已得时间”,任务才有资格(称eligible)被挑选;进一步地,在有资格被挑选的任务中,选择任务完成时限(deadline)最近的任务运行,即所谓 Earliest Eligible Virtual Deadline First,缩写为 EEVDF。

EEVDF 论文

本节主要梳理论文中的关键概念,与Linux实现息息相关的部分:

Vt:系统虚拟时间

任务 i 在 [ t 1 , t 2 ] [t_1,t_2] [t1,t2] 时间段,应得运行时间为:
S ⁡ ( t 1 , t 2 ) = ∫ t 1 t 2 w i W (1) \operatorname{S}(t_1, t_2) = \int_{t_1}^{t_2} \frac{w_i}{W} \tag{1} S(t1,t2)=t1t2Wwi(1)
其中:

  1. w i w_i wi 是任务 i 的权重。
  2. W W W 是系统中所有活跃任务权重的累加;

令系统虚拟时间为:
V t = ∫ 0 t 1 W (2) V_t = \int_{0}^{t} \frac{1}{W} \tag{2} Vt=0tW1(2)
则式(1)又可以写作:
S ⁡ ( t 1 , t 2 ) = ∫ t 1 t 2 w i W = w i ∫ t 1 t 2 1 W = w i ( V t 2 − V t 1 ) (3) \begin{align} \operatorname{S}(t_1, t_2) & = \int_{t_1}^{t_2} \frac{w_i}{W} \\ & = w_i \int_{t_1}^{t_2} \frac{1}{W} \\ & = w_i(V_{t_2}-V_{t_1}) \end{align} \tag{3} S(t1,t2)=t1t2Wwi=wit1t2W1=wi(Vt2Vt1)(3)

ve:eligible time

任务 i 请求的 eligible time 被定义为:任务 i 的应得时间,和 i 在发起该请求前已得时间相等的那一刻,记该虚拟时间为 eligible time。当系统虚拟时间 V t V_t Vt 大于等于 V e V_e Ve,称任务 i 的请求 eligible,即任务 i 有资格作为上 CPU 运行的候选任务。

记任务 i 在 [ t 1 , t 2 ] [t_1,t_2] [t1,t2] 时间段实际获得的运行时间为 s(t1,t2),根据 eligible time 的定义,有:
S ⁡ ( t 0 , e ) = s ⁡ ( t 0 , t ) (4) \begin{array}{c} \operatorname{S}(t_0, e) = \operatorname{s}(t_0, t) \tag{4} \end{array} S(t0,e)=s(t0,t)(4)
其中 t 0 t_0 t0 是任务 i 开始活跃的时间, t t t 为任务 i 发起新请求的时间,结合公式(3),可推出 eligible time 的计算方式:
S ⁡ ( t 0 , e ) = s ⁡ ( t 0 , t ) = > w i ( V e − V t 0 ) = s ⁡ ( t 0 , t ) = > V e = V t 0 + s ( t 0 , t ) w i (5) \begin{array}{c} \operatorname{S}(t_0, e) = \operatorname{s}(t_0, t)\\ = > w_i(V_e-V_{t_0}) = \operatorname{s}(t_0, t) \\ = > V_e = V_{t_0} + \frac{s(t_0, t)}{w_i} \tag{5} \end{array} S(t0,e)=s(t0,t)=>wi(VeVt0)=s(t0,t)=>Ve=Vt0+wis(t0,t)(5)

vd:deadline time

任务 i 请求的 deadline time 被定义为:任务 i 在虚拟时间间隔 [ V e , V d ] [V_e,V_d] [Ve,Vd] 之间可得的运行时间,等于请求的长度 r。有:
S ⁡ ( e , d ) = r \begin{array}{c} \operatorname{S}(e, d) = r \end{array} S(e,d)=r
结合公式(3),可推出 deadline time 的计算方式如下:
S ⁡ ( e , d ) = r = > w i ( V d − V e ) = r = > V d = V e + r w i (6) \begin{array}{c} \operatorname{S}(e, d) = r \\ = > w_i(V_d-V_e) = r \\ = > V_d = V_e + \frac{r}{w_i} \tag{6} \end{array} S(e,d)=r=>wi(VdVe)=r=>Vd=Ve+wir(6)

ve 和 vd 递推公式

根据式(5)和式(6),对于任务的多个请求,有以下递推公式:
V e 1 = V t 0 V d 1 = V t 0 + r w i . . . V e k = V d k − 1 V d k = V e k + r w i (7) \begin{array}{c} V_e^1 = V_{t_0} \\ V_d^1 = V_{t_0} + \frac{r}{w_i} \\ ... \\ V_e^k = V_d^{k-1} \\ V_d^k = V_e^{k} + \frac{r}{w_i} \tag{7} \end{array} Ve1=Vt0Vd1=Vt0+wir...Vek=Vdk1Vdk=Vek+wir(7)

ve 和 vd 推算例子

假设:

  1. c1 和 c2 的权重相等,有 w 1 = w 2 = 2 w_1 = w_2 = 2 w1=w2=2
  2. c1 在 t=0 加入竞争,c2 在 t=1 加入竞争;
  3. c1 的请求长度 r 1 = 2 r_1=2 r1=2,c2 的请求长度 r 2 = 1 r_2=1 r2=1
    在这里插入图片描述

推算详解:
在这里插入图片描述

lag:为了公平

任务 i 应得时间和已得时间不总是相等的,两者的差值称作 lag:
l a g i ( t ) = S i ( t 0 , t ) − s i ( t 0 , t ) (8) \begin{array}{c} lag_i(t) = S_i(t_0,t) - s_i(t_0,t) \tag{8} \end{array} lagi(t)=Si(t0,t)si(t0,t)(8)
当:

  1. 当系统虚拟时间和任务的 eligible time 相等,即 V t = V e V_t = V_e Vt=Ve,则任务的 lag 为 0。
  2. S i S_i Si 大于 s i s_i si,说明 [ t 0 , t ] [t_0,t] [t0,t] 时间段内,任务 i 已得时间比应得时间少,此时 lag 为正,有 V t > V e V_t > V_e Vt>Ve,则任务 i 一旦有新的请求,该请求会立马进入 eligible 状态。
  3. S i S_i Si 小于 s i s_i si,说明 [ t 0 , t ] [t_0,t] [t0,t] 时间段内,任务 i 已得时间比应得时间多,此时 lag 为负,有 V t < V e V_t < V_e Vt<Ve,则任务 i 需要等到系统虚拟时间 V t V_t Vt 增长到 V e V_e Ve,才进入 eligible 状态,为其它任务提供“catch up”的机会。

任务可能在 lag 不为 0 的场景下加入或者退出竞争(两种行为是对称的,以下只讨论退出竞争场景)。为了保证公平,EEVDF 算法通过将退出竞争任务的 lag(该任务透支的,或残余的时间)按各自权重大小,反映到剩余活跃的任务上。以上逻辑通过更新系统虚拟时间 V t V_t Vt 和其它任务的 lag 实现。推导过程如下:

系统虚拟时间 Vt 更新

假设任务 a 在时间 t 退出竞争,取无限小时间间隔 δ δ δ,令 t + t^+ t+ t + δ t+δ t+δ,则系统中其它任务 i 在时间 [ t 0 , t + ] [t_0,t^+] [t0,t+] 内可分配的运行时间可表示为:
S i ( t 0 , t + ) = ( t − t 0 − s a ( t 0 , t ) ) w i W (9) \begin{array}{c} S_i(t_0,t^+) = (t - t_0 -s_a(t_0,t)) \frac{w_i}{W} \tag{9} \end{array} Si(t0,t+)=(tt0sa(t0,t))Wwi(9)
其中 W W W 为任务 a 退出竞争后系统中所有活跃任务权重的累加。将式(10)中 s a ( t 0 , t ) s_a(t_0, t) sa(t0,t) 替换由任务 a 的 lag 表示,由此推出任务 a 在退出竞争后系统虚拟时间 V t V_t Vt 会被更新为:
∵ l a g a ( t ) = ( t − t 0 ) w a W + w a − s a ( t 0 , t ) ∴ s a ( t 0 , t ) = ( t − t 0 ) w a W + w a − l a g a ( t ) ∴ S i ( t 0 , t + ) = [ t − t 0 − ( t − t 0 ) w a W + w a + l a g a ( t ) ] w i W = [ ( t − t 0 ) W W + w a + l a g a ( t ) ] w i W = ( t − t 0 ) w i W + w a + l a g a ( t ) w i W = w i ( V t − V t 0 ) + l a g a ( t ) w i W ∵ S i ( t 0 , t + ) = w i ( V t + − V t 0 ) = w i ( V t − V t 0 ) + l a g a ( t ) w i W ∴ V t + = V t + l a g a ( t ) W \begin{align} \because lag_a(t) & = (t - t_0) \frac{w_a}{W + w_a} - s_a(t_0,t) \\ \therefore s_a(t_0,t) & = (t - t_0) \frac{w_a}{W + w_a} - lag_a(t) \\ \therefore S_i(t_0,t^+) & = [t - t_0 - (t - t_0) \frac{w_a}{W + w_a} + lag_a(t)] \frac{w_i}{W} \\ & = [(t - t_0) \frac{W}{W + w_a} + lag_a(t)] \frac{w_i}{W} \\ & = (t - t_0) \frac{w_i}{W + w_a} + lag_a(t) \frac{w_i}{W} \\ & = w_i(V_t - V_{t_0}) + lag_a(t) \frac{w_i}{W} \\ \because S_i(t_0,t^+) & = w_i(V_{t^+} - V_{t_0}) = w_i(V_t - V_{t_0}) + lag_a(t) \frac{w_i}{W} \\ \therefore V_{t^+} & = V_t + \frac{lag_a(t)}{W} \tag{10} \end{align} laga(t)sa(t0,t)Si(t0,t+)Si(t0,t+)Vt+=(tt0)W+wawasa(t0,t)=(tt0)W+wawalaga(t)=[tt0(tt0)W+wawa+laga(t)]Wwi=[(tt0)W+waW+laga(t)]Wwi=(tt0)W+wawi+laga(t)Wwi=wi(VtVt0)+laga(t)Wwi=wi(Vt+Vt0)=wi(VtVt0)+laga(t)Wwi=Vt+Wlaga(t)(10)

任务 lag 更新

代入式(10),任务 a 在退出竞争后其它任务的 lag 会被更新为:
l a g i ( t + ) = w i ( V t + − V t 0 ) − s i ( t 0 , t + ) = w i ( V t + l a g a ( t ) W − V t 0 ) − s i ( t 0 , t + ) = w i ( V t − V t 0 ) − s i ( t 0 , t + ) + w i l a g a ( t ) W ∵ s i ( t 0 , t + ) ⟶ s i ( t 0 , t ) ∴ l a g i ( t + ) = l a g i ( t ) + w i l a g a ( t ) W \begin{align} lag_i(t^+) &= w_i(V_{t^+} - V_{t_0}) - s_i(t_0,t^+) \\ & = w_i(V_t + \frac{lag_a(t)}{W} - V_{t_0}) - s_i(t_0,t^+) \\ & = w_i(V_t - V_{t_0}) - s_i(t_0,t^+) + w_i \frac{lag_a(t)}{W} \\ \because s_i(t_0,t^+) &\longrightarrow s_i(t_0,t) \\ \therefore lag_i(t^+) &= lag_i(t) + w_i \frac{lag_a(t)}{W} \tag{11} \end{align} lagi(t+)si(t0,t+)lagi(t+)=wi(Vt+Vt0)si(t0,t+)=wi(Vt+Wlaga(t)Vt0)si(t0,t+)=wi(VtVt0)si(t0,t+)+wiWlaga(t)si(t0,t)=lagi(t)+wiWlaga(t)(11)
可以发现非常自然地,退出竞争任务 a 的 l a g a ( t ) lag_a(t) laga(t) 按剩余任务各自的权重大小,均匀地作用到其它任务的 lag 上。

EEVDF调度类原始提交

以下梳理 Linux 引入 EEVDF 调度器的初始提交中关键的三个patch:

sched/fair: Add cfs_rq::avg_vruntime

该提交引入 cfs_rq→avg_vruntimecfs_rq→avg_load,目的是为了方便计算系统虚拟时间 V t V_t Vt(该变量在在代码中叫做avg_vruntime,注意区分 cfs_rq→avg_vruntimeavg_vruntime)。系统虚拟时间的计算方式从论文的以下引理开始推导:

Lemma 2 At any moment of time,the sum of the lags of all active client is zero.

有:
∑ l a g i ( t ) = 0 (12) \begin{array}{c} \sum lag_i(t) = 0 \tag{12} \end{array} lagi(t)=0(12)
将 lag 的计算公式(8)代入后可以得到:
∑ l a g i ( t ) = ∑ [ S i ( t 0 , t ) − s i ( t 0 , t ) ] = 0 (13) \begin{array}{c} \sum lag_i(t) = \sum [S_i(t_0,t) - s_i(t_0,t)] = 0 \tag{13} \end{array} lagi(t)=[Si(t0,t)si(t0,t)]=0(13)
原 CFS 调度器中已经记录了调度单元实际运行的时间,即se->vruntime,记 se->vruntime V t i V_{t_i} Vti,则有:
∑ l a g i ( t ) = ∑ [ S i ( t 0 , t ) − s i ( t 0 , t ) ] = ∑ [ S i ( t 0 , t ) − S i ( t 0 , t i ) ] = ∑ [ S i ( t 0 , t ) − S i ( t 0 , t i ) ] = ∑ [ w i ( V t − V t 0 ) − w i ( V t i − V t 0 ) ] = ∑ [ w i ( V t − V t i ) ] = ∑ w i V t − ∑ w i V t i \begin{align} \sum lag_i(t) & = \sum [S_i(t_0,t) - s_i(t_0,t)] \\ & = \sum [S_i(t_0,t) - S_i(t_0,t_i)] \\ & = \sum [S_i(t_0,t) - S_i(t_0,t_i)] \\ & = \sum [w_i(V_t - V_{t_0}) - w_i(V_{t_i} - V_{t_0})] \\ & = \sum [w_i(V_t - V_{t_i})] \\ & = \sum w_iV_t - \sum w_iV_{t_i} \tag{14}\\ \end{align} lagi(t)=[Si(t0,t)si(t0,t)]=[Si(t0,t)Si(t0,ti)]=[Si(t0,t)Si(t0,ti)]=[wi(VtVt0)wi(VtiVt0)]=[wi(VtVti)]=wiVtwiVti(14)
又因式(14)=0,移项之后, V t V_t Vt 可以表示如下:
V t = ∑ w i V t i W \begin{align} V_t = \frac{\sum w_iV_{t_i}}{W} \tag{15} \end{align} Vt=WwiVti(15)
V t V_t Vt 可以表示为所有活跃任务 vruntime 按权重占比的加权和。但在具体实现上,任务 vruntime 的数据类型是 u64,进行乘法运算容易导致溢出。记 cfs->min_vruntime V 0 V_0 V0,进行以下等价替换:
V t i = ( V t i − V 0 ) + V 0 \begin{align} V_{t_i} = (V_{t_i} - V_0) + V_0 \tag{16} \end{align} Vti=(VtiV0)+V0(16)
将式(16)代入式(15),则有:
V t = ∑ w i V t i W = ∑ w i [ ( V t i − V 0 ) + V 0 ] W = ∑ w i ( V t i − V 0 ) + V 0 ∑ w i W = ∑ w i ( V t i − V 0 ) W + V 0 \begin{align} V_t & = \frac{\sum w_iV_{t_i}}{W} \\ & = \frac{\sum w_i[ (V_{t_i} - V_0) + V_0]}{W} \\ & = \frac{\sum w_i(V_{t_i} - V_0) + V_0\sum w_i}{W} \\ & = \frac{\sum w_i (V_{t_i} - V_0)}{W} + V_0 \tag{17} \end{align} Vt=WwiVti=Wwi[(VtiV0)+V0]=Wwi(VtiV0)+V0wi=Wwi(VtiV0)+V0(17)
代码实现中,式(17)被切分为3个部分,分别由三个变量追踪,第一个是已有的变量,后两个是本次提交新引入的变量:

  1. cfs->min_vruntime:式中 V 0 V_0 V0
  2. cfs_rq→avg_vruntime:式中 ∑ w i ( V t i − V 0 ) \sum w_i (V_{t_i} - V_0) wi(VtiV0)
  3. cfs_rq→avg_load:式中 W W W

具体实现中,通过 avg_vruntime() 函数获取当前系统虚拟时间 V t V_t Vt,具体计算逻辑如下:

|- avg_vruntime
	|- s64 avg = cfs_rq->avg_vruntime;
	|- long load = cfs_rq->avg_load;
	|- unsigned long weight = scale_load_down(curr->load.weight);
	// cfs_rq->avg_vruntime 累加 curr se 的贡献,其中 entity_key 用于计算 v_i - v0 
	|- avg += entity_key(cfs_rq, curr) * weight
	|- load += weight;
	// 返回 V
	|- return cfs_rq->min_vruntime + avg / load;

以下场景需要同步对新引入变量进行更新:

// 1. 更新 cfs_rq->min_vruntime 场景,同步更新 cfs_rq->avg_vruntime
|- [update_curr|dequeue_task_fair]
	// 更新 cfs_rq->min_vruntime
	|- update_min_vruntime -> __update_min_vruntime
		// 更新 cfs_rq->avg_vruntime,以下时序关系为 v0 -> v0' -> vi,min_vruntime 将从 v0 更新为 v0'
		// avg_vruntime_new = \Sum (v_i - v0') * w_i
		//                  = \Sum ((v_i - v0) - (v0' - v0)) * w_i
		//                  = \Sum (v_i - v0) * w_i - \Sum (v0' - v0) * w_i
		//                  = avg_vruntime_old - (v0' - v0) * avg_load
		|- avg_vruntime_update
			|- cfs_rq->avg_vruntime -= cfs_rq->avg_load * delta;

// 2. 任务入队出队场景,更新 cfs_rq->avg_vruntime 和 cfs_rq->avg_load
|- avg_vruntime_add
	|- cfs_rq->avg_vruntime += entity_key(cfs_rq, se) * weight;
	|- cfs_rq->avg_load += weight;
sched/fair: Add lag based placement

关于任务持 lag 重新加入竞争场景,为了保证相对公平,论文提供了多种实现策略,当前提交实现了以下两种:

  1. V t V_t Vt 随加入和退出调度实体的 lag 进行更新(参考式10):该策略适合希望在任务活动的多个阶段保持公平的系统。
  2. 调度实体的 lag 在退出竞争后清 0,重新加入竞争不处理 lag:该策略适合任务活动的触发是独立事件的系统。

策略可通过调度特性 sched_feat PLACE_LAG 的值进行控制,当前默认采用策略 1。上文我们已经推导了任务 a 退出竞争时, V t V_t Vt 的更新公式——式(10),也推导了其它任务 a 在 V t V_t Vt 更新后lag值的变化。同理可推导出任务 a 加入竞争后, V t V_t Vt 的更新公式:
V t + = V t − l a g a ( t ) W + w a \begin{align} V_{t^+} & = V_t - \frac{lag_a(t)}{W + w_a} \tag{18} \end{align} Vt+=VtW+walaga(t)(18)
当前提交为了追踪每个调度实体的 lag,为 sched_entity 引入了新成员 se->vlag(以下记作 v l ( t ) vl(t) vl(t)),原 lag 计算公式为 w i ( V t − V t i ) w_i(V_t - V_{t_i}) wi(VtVti),为了防止到处都是 w i w_i wise->vlag 仅记录公式中 vruntime 的差值部分,即 v l ( t ) = V t − V t i vl(t) = V_t - V_{t_i} vl(t)=VtVti。则式(18)可改写为:
V t + = V t − w a ∗ v l a ( t ) W + w a \begin{align} V_{t^+} & = V_t - \frac{w_a * vl_a(t)}{W + w_a} \tag{19} \end{align} Vt+=VtW+wawavla(t)(19)
V t V_t Vt 更新后,代入式(19),在加入竞争后,任务 a 的 vlag 将被更新为:
v l a ( t + ) = V t + − V t i = V t − V t i − w a ∗ v l a ( t ) W + w a = v l a ( t ) − w a ∗ v l a ( t ) W + w a \begin{align} vl_a(t^+) & = V_{t^+} - V_{t_i} \\ & = V_t - V_{t_i} - \frac{w_a * vl_a(t)}{W + w_a} \\ & = vl_a(t) - \frac{w_a * vl_a(t)}{W + w_a} \tag{20} \end{align} vla(t+)=Vt+Vti=VtVtiW+wawavla(t)=vla(t)W+wawavla(t)(20)
可以发现 v l a ( t + ) vl_a(t^+) vla(t+) 严格小于 v l a ( t ) vl_a(t) vla(t),这会导致任务 a 在频繁加入退出竞争场景中 v l a ( t ) vl_a(t) vla(t) 急速衰减,策略一会退化为策略二。为了保证任务 a 的 v l a ( t ) vl_a(t) vla(t) 在加入竞争前后保持不变,我们需要将 v l a ( t ) vl_a(t) vla(t) 提前放大,使其衰减之后恰好等于 v l a ( t ) vl_a(t) vla(t)。通过变换式(20),可知 v l a ( t ) vl_a(t) vla(t) 应当放大为:
∵ v l a ( t + ) = v l a ( t ) − w a ∗ v l a ( t ) W + w a ∴ v l a ( t + ) = [ ( W + w a ) v l a ( t ) − w a ∗ v l a ( t ) ] W + w a ∴ v l a ( t + ) = W ∗ v l a ( t ) W + w a ∴ W ∗ v l a ( t ) = ( W + w a ) v l a ( t + ) ∴ v l a ( t ) = ( W + w a ) v l a ( t + ) W (21) \begin{array}{c} \because vl_a(t^+) = vl_a(t) - \frac{w_a * vl_a(t)}{W + w_a} \\ \therefore vl_a(t^+) = \frac{[(W + w_a)vl_a(t) - w_a * vl_a(t)]}{W + w_a} \\ \therefore vl_a(t^+) = \frac{W*vl_a(t)}{W + w_a} \\ \therefore W*vl_a(t) = (W + w_a)vl_a(t^+) \\ \therefore vl_a(t) = \frac{(W + w_a)vl_a(t^+)}{W} \tag{21} \end{array} vla(t+)=vla(t)W+wawavla(t)vla(t+)=W+wa[(W+wa)vla(t)wavla(t)]vla(t+)=W+waWvla(t)Wvla(t)=(W+wa)vla(t+)vla(t)=W(W+wa)vla(t+)(21)
在加入竞争前(place_entity函数中),通过赋值 V t i = V t − ( W + w a ) v l a ( t ) W V_{t_i} = V_t - \frac{(W + w_a)vl_a(t)}{W} Vti=VtW(W+wa)vla(t) ,使原始 vlag v l a ( t ) vl_a(t) vla(t) 被放大为 ( W + w a ) v l a ( t ) W \frac{(W + w_a)vl_a(t)}{W} W(W+wa)vla(t),通过将 ( W + w a ) v l a ( t ) W \frac{(W + w_a)vl_a(t)}{W} W(W+wa)vla(t) 代入式(20),可得 v l a ( t + ) = v l a ( t ) vl_a(t^+) =vl_a(t) vla(t+)=vla(t),实现加入竞争前后vlag保持不变的效果。

相关逻辑如下

|- place_entity
	|- u64 vruntime = avg_vruntime(cfs_rq);
	|- s64 lag = 0; // 用于保存放大后的 vlag
	|- if (sched_feat(PLACE_LAG) && cfs_rq->nr_running) // 策略 1 分支
		lag = se->vlag; // 原始 vlag
		load = cfs_rq->avg_load; // W
		lag *= load + scale_load_down(se->load.weight); // vlag * (W + wi)
		lag = div_s64(lag, load); // vlag * (W + wi) / W
	|- se->vruntime = vruntime - lag; // 使后续 vruntime - se->vruntime 可以拿到放大后的 vlag,即 vlag * (W + wi) / W

以下场景需要同步对vlag进行修改:

// 1. 调度实体离开运行队列,保存lag(策略一需求)
|- dequeue_entity
	|- update_entity_lag(cfs_rq, se);

// 2.更改调度实体的权重,因为lag需要保持不变,vlag需要同步做缩放
|- reweight_entity
	|- se->vlag = div_s64(se->vlag * old_weight, weight);
sched/fair: Implement an EEVDF-like scheduling policy

Linux 原调度器 CFS 可调参数是调度实体的权重值,用于控制调度实体之间运行时间的分配比例;当引入另一个用于控制时延的参数时,系统采用类似于 WF2Q 或者 EEVDF 的调度器会更合适。具体而言,EEVDF 调度器有以下两个参数:

  1. weight:权重值,保留 CFS 调度器的实现,由 nice 值控制。
  2. slice length:请求长度,用于计算 deadline。

通过为任务设置一个小的 slice,相应的该任务请求的 deadline 也会提前,因为 EEVDF 会选择 eligible 的任务中 deadline 最小的任务运行,所以该任务可以更早地被选择运行。

当前提交改动包括:

  1. 为调度实体引入 slice 用于计算 deadline,并新增维护该 deadline 的相关逻辑。
  2. 实现 EEVDF 选调度实体的逻辑 pick_eevdf,添加任务抢占相关逻辑。

数据结构的修改:

struct sched_entity {
// 当前调度实体的 deadline
+       u64                             deadline;
// EEVDF 为了在红黑树内部快速查找目标调度实体(eligible 中 deadline 最小的),
// 引入 min_deadline 用于记录以调度实体为根的子树中最小的 deadline 值
+       u64                             min_deadline;
// 当前调度实体的请求长度
+       u64                             slice;

为保证 se->min_deadline 保存的是子树中最小的 deadline,新增回调函数(min_deadline_update()),在红黑树节点插入删除节点(__enqueue_entity/__dequeue_entity)时被调用,该回调函数顺着红黑树递归更新红黑树中调度实体的 min_deadline。

新增函数 entity_eligible 判断调度实体是否 eligible,根据上文的推断,调度实体 eligible 和 vlag 大于等于 0 是等价的,即 V t − V t i > = 0 V_t - V_{t_i} >= 0 VtVti>=0,回忆式(17),
V t = ∑ w i ( V t i − V 0 ) W + V 0 \begin{align} V_t = \frac{\sum w_i (V_{t_i} - V_0)}{W} + V_0 \end{align} Vt=Wwi(VtiV0)+V0
则 eligible 等价于令上式大于等于 V t i V_{t_i} Vti (即se->vruntime),为不丢失精度,进行移项去除除号,实现如下:

|- entity_eligible
	|- s64 avg = cfs_rq->avg_vruntime; // \sum w_i (V_{t_i} - V_0)
	|- long load = cfs_rq->avg_load; // W
	|- if (curr && curr->on_rq)
		avg += entity_key(cfs_rq, curr) * weight; // avg 考虑当前调度实体
		load += weight; // load 考虑当前调度实体
	|- return avg >= entity_key(cfs_rq, se) * load; // \sum w_i (V_{t_i} - V_0) >= (V_t - V_0)* W

调度实体的 deadline 在 update_curr 时被更新:

|- update_curr -> update_deadline(cfs_rq, curr);
	// se->slice 可由 /sys/kernel/debug/sched/base_slice_ns 控制,当前该值对于每个任务是一致的
	// vd_i = ve_i + r_i / w_i,calc_delta_fair
	|- se->deadline = se->vruntime + calc_delta_fair(se->slice, se);

pick_eevdf 用于选出红黑树中处于 eligible 状态中 deadline 最小的那个调度实体:

|- pick_eevdf
	// 如果当前任务没有入队,或者当前任务不再 eligible,当前任务不作为备选
	|- if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr))) curr = NULL;
	// 从红黑树根节点开始遍历
	while (node) {
		struct sched_entity *se = __node_2_se(node);

		// 红黑树以 vruntime 进行排序,所以右子树的 vruntime 一定比当前节点的 vruntime 大
		// 而 eligible 条件是 V - vruntime > 0,如果当前节点都不 eligible,右子树必定不 eligible
		// 应当在左子树进行查找
		if (!entity_eligible(cfs_rq, se)) {
			node = node->rb_left;
			continue;
		}

		// best 变量用于追踪 eligible 节点中 deadline 最小的节点
		// 如果搜索过程中发现有 deadline 更小的,赋值
		|- if (!best || deadline_gt(deadline, best, se))
			best = se;
			// 如果该节点的 deadline 等于子树中 deadline 最小的值,说明其本身是子树中 deadline 最小的,结束查找
			if (best->deadline == best->min_deadline) break;

		// 最小的 deadline 在左子树,在左子树继续搜索
		if (node->rb_left && __node_2_se(node->rb_left)->min_deadline == se->min_deadline)
			node = node->rb_left;
			continue;

		// 最小的 deadline 在右子树,在右子树继续搜索(当前有bug,在优化一节会修正)
		node = node->rb_right;
	}

	// 如果没搜索到,或者 curr 的 deadline 比 best 要更小,选择 curr
	|- if (!best || (curr && deadline_gt(deadline, best, curr))) best = curr;
	|- return best;

关于任务抢占行为逻辑增加:

// 1. tick中抢占检测由slice的完成驱动,任务的slice消耗完毕后,设置重调度标记
|- entity_tick -> update_curr(cfs_rq);
	|- update_deadline(cfs_rq, curr)
		// slice 还有剩余,无需更新(递推)下一个slice的结束时间,直接返回
		|- if ((s64)(se->vruntime - se->deadline) < 0) return;
		// 更新 deadline
		|- se->deadline = se->vruntime + calc_delta_fair(se->slice, se);
		// 运行队列存在其它任务,设置重调度标记
		|- if (cfs_rq->nr_running > 1) resched_curr(rq_of(cfs_rq));

// 2. 唤醒中抢占检测由 deadline 驱动
|- ttwu_do_wakeup -> check_preempt_curr(rq, p, wake_flags) -> check_preempt_wakeup
	// 如果被唤醒的任务 p 是 RBT 的 eligible 任务中 deadline 最早的,设置重调度标记
	|- if (pick_eevdf(cfs_rq) == pse) resched_curr(rq);

修正了 update_entity_lag() 的实现逻辑, l a g i = S − s i = w i ∗ ( V − v i ) lag_i = S - s_i = w_i * (V - v_i) lagi=Ssi=wi(Vvi),因为系统虚拟时间会随调度实体的加入/退出/重新设置权重值而前后移动,可能会导致 lag 比原本的要小。根据论文的 Theorem 1,在稳定系统中每个任务的 lag 有上下界:
− r m a x < l a g < m a x ( r m a x , q ) (22) -r_{max} < lag < max(r_{max}, q) \tag{22} rmax<lag<max(rmax,q)(22)
式中 r m a x r_{max} rmax 是任务发起请求中最大的请求长度,实现中取做两倍 se->slice q q q 代表 time quantum 的大小,实现中取 TICK_NSEC(系统计时粒度)。逻辑更改如下:

|- update_entity_lag
	|- lag = avg_vruntime(cfs_rq) - se->vruntime; // 获取 lag
	|- limit = max_t(u64, 2*se->slice, TICK_NSEC); // 确认上下界
	|- se->vlag = clamp(lag, -limit, limit); // 将 lag 界定在上下界中

EEVDF调度类相关优化

以下简单介绍两个相关优化:

sched/eevdf: Fix vruntime adjustment on reweight

当任务的权重从 w 更新为 w’,等价于 w 权重任务的出队,以及 w’ 权重任务的入队。在提交 sched/fair: Add lag based placement 中我们得知任务的出入队应当保证 vlag 不变,为了满足该需求,任务权重的修改需要同步修改 vruntime 和 deadline。

推导过程在代码注释中相当详尽,这里仅做概括:

  1. 通过归谬法证明:假设任务的 vruntime 在 reweight 前后不需要修正,根据 reweight 前后 vlag 应该保持不变这个条件,会推导出 w=w’,与假设相悖,因此 reweight 前后任务的 vruntime 需要进行修正。
  2. 根据 reweight 前后 vlag 应该保持不变这个条件,可以推导出系统虚拟时间在 reweight 前后应当保持一致。
  3. 根据 2 的结论,推导出任务 vruntime 的更新公式。
  4. 通过替换 d’ = v’ + r/w’ 公式中的 v’ 和 r,并结合结论 2,推导出 deadline 的更新方式。
sched/eevdf: Sort the rbtree by virtual deadline

在提交 sched/fair: Implement an EEVDF-like scheduling policy 中引入了 pick_eevdf() 函数用于查找 RBT 中 eligible 的调度实体中 deadline 最小的调度实体。但因为遍历 RBT 过程中,在以找到 min_deadline 为目标挑选左右子树的过程中,可能导致返回的调度实体并不 eligible。

提交 sched/eevdf: Fix pick_eevdf() 尝试对其进行修复,但需要多次查找,引入了较高的时间复杂度。

当前提交和 sched/eevdf: O(1) fastpath for task selection 通过修改数据结构,使 cfs_rq 将任务 deadline 作为红黑树的键,而引入 min_vruntime 记录子树 vruntime 最小的值。跟初始方案对比,调换了 cfs_rq 中 deadline 和 vruntime 的排序方式。相应地 pick_eevdf() 被修正为:

|- pick_eevdf
	struct sched_entity *se = __pick_first_entity(cfs_rq); // deadline 最小的任务
	// 如果 deadline 最小的任务 eligible,获取成功
	|- if (se && entity_eligible(cfs_rq, se))
		best = se;
		goto found;
	
	// 堆搜索
	|- while (node) {
		|- struct rb_node *left = node->rb_left;

		// 如果左子树存在 eligible 节点,因为左子树的 deadline 总是更小,应当在左子树进行查找
		|- if (left && vruntime_eligible(cfs_rq, __node_2_se(left)->min_vruntime))
			node = left;
			continue;

		// 走到这,左子树为空或者不包含 eligible 节点,当前节点是 deadline 最小的
		// 如果当前节点 eligible,应当选择当前节点
		|- if (entity_eligible(cfs_rq, se))
			best = se;
			break;

		// 左子树和当前节点都不符合要求,在右子树继续搜索
		|- node = node->rb_right;

	// 如果没搜索到,或者 curr 的 deadline 比 best 要更小,选择 curr
	|- if (!best || (curr && deadline_gt(deadline, best, curr))) best = curr;
	|- return best;

在这里插入图片描述

ref

  1. Earliest Eligible Virtual Deadline First : A Flexible and Accurate Mechanism for Proportional Share Resource Allocation
  2. An EEVDF CPU scheduler for Linux
  3. Linux 核心設計: Scheduler(5): EEVDF 排程器
  4. Linux 核心專題: CPU 排程器研究
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值