本文主要总结了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)
其中:
- w i w_i wi 是任务 i 的权重。
- 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=wi∫t1t2W1=wi(Vt2−Vt1)(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(Ve−Vt0)=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(Vd−Ve)=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=Vdk−1Vdk=Vek+wir(7)
ve 和 vd 推算例子
假设:
- c1 和 c2 的权重相等,有 w 1 = w 2 = 2 w_1 = w_2 = 2 w1=w2=2;
- c1 在 t=0 加入竞争,c2 在 t=1 加入竞争;
- 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)
当:
- 当系统虚拟时间和任务的 eligible time 相等,即 V t = V e V_t = V_e Vt=Ve,则任务的 lag 为 0。
- 当 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 状态。
- 当 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+)=(t−t0−sa(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+=(t−t0)W+wawa−sa(t0,t)=(t−t0)W+wawa−laga(t)=[t−t0−(t−t0)W+wawa+laga(t)]Wwi=[(t−t0)W+waW+laga(t)]Wwi=(t−t0)W+wawi+laga(t)Wwi=wi(Vt−Vt0)+laga(t)Wwi=wi(Vt+−Vt0)=wi(Vt−Vt0)+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(Vt−Vt0)−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_vruntime
和 cfs_rq→avg_load
,目的是为了方便计算系统虚拟时间 V t V_t Vt(该变量在在代码中叫做avg_vruntime
,注意区分 cfs_rq→avg_vruntime
和 avg_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(Vt−Vt0)−wi(Vti−Vt0)]=∑[wi(Vt−Vti)]=∑wiVt−∑wiVti(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=W∑wiVti(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=(Vti−V0)+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=W∑wiVti=W∑wi[(Vti−V0)+V0]=W∑wi(Vti−V0)+V0∑wi=W∑wi(Vti−V0)+V0(17)
代码实现中,式(17)被切分为3个部分,分别由三个变量追踪,第一个是已有的变量,后两个是本次提交新引入的变量:
cfs->min_vruntime
:式中 V 0 V_0 V0cfs_rq→avg_vruntime
:式中 ∑ w i ( V t i − V 0 ) \sum w_i (V_{t_i} - V_0) ∑wi(Vti−V0)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 重新加入竞争场景,为了保证相对公平,论文提供了多种实现策略,当前提交实现了以下两种:
- V t V_t Vt 随加入和退出调度实体的 lag 进行更新(参考式10):该策略适合希望在任务活动的多个阶段保持公平的系统。
- 调度实体的 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+=Vt−W+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(Vt−Vti),为了防止到处都是 w i w_i wi,se->vlag
仅记录公式中 vruntime 的差值部分,即 v l ( t ) = V t − V t i vl(t) = V_t - V_{t_i} vl(t)=Vt−Vti。则式(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+=Vt−W+wawa∗vla(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=Vt−Vti−W+wawa∗vla(t)=vla(t)−W+wawa∗vla(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+wawa∗vla(t)∴vla(t+)=W+wa[(W+wa)vla(t)−wa∗vla(t)]∴vla(t+)=W+waW∗vla(t)∴W∗vla(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=Vt−W(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; // 原始 vlagload = cfs_rq->avg_load; // Wlag *= 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 调度器有以下两个参数:
- weight:权重值,保留 CFS 调度器的实现,由 nice 值控制。
- slice length:请求长度,用于计算 deadline。
通过为任务设置一个小的 slice,相应的该任务请求的 deadline 也会提前,因为 EEVDF 会选择 eligible 的任务中 deadline 最小的任务运行,所以该任务可以更早地被选择运行。
当前提交改动包括:
- 为调度实体引入 slice 用于计算 deadline,并新增维护该 deadline 的相关逻辑。
- 实现 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 Vt−Vti>=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=W∑wi(Vti−V0)+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=S−si=wi∗(V−vi),因为系统虚拟时间会随调度实体的加入/退出/重新设置权重值而前后移动,可能会导致 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。
推导过程在代码注释中相当详尽,这里仅做概括:
- 通过归谬法证明:假设任务的 vruntime 在 reweight 前后不需要修正,根据 reweight 前后 vlag 应该保持不变这个条件,会推导出 w=w’,与假设相悖,因此 reweight 前后任务的 vruntime 需要进行修正。
- 根据 reweight 前后 vlag 应该保持不变这个条件,可以推导出系统虚拟时间在 reweight 前后应当保持一致。
- 根据 2 的结论,推导出任务 vruntime 的更新公式。
- 通过替换 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_eevdfstruct 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
- Earliest Eligible Virtual Deadline First : A Flexible and Accurate Mechanism for Proportional Share Resource Allocation
- An EEVDF CPU scheduler for Linux
- Linux 核心設計: Scheduler(5): EEVDF 排程器
- Linux 核心專題: CPU 排程器研究