kernel 6.6中新增的EEVDF特性
一.pick_next_entity 选择下一个任务
/*
5393 * Pick the next process, keeping these things in mind, in this order:
5394 * 1) keep things fair between processes/task groups
5395 * 2) pick the "next" process, since someone really wants that to run
5396 * 3) pick the "last" process, for cache locality
5397 * 4) do not run the "skip" process, if something else is available
5398 */
5399 static struct sched_entity *
5400 pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
5401 {
5402 /*
5403 * Enabling NEXT_BUDDY will affect latency but not fairness.
5404 */
5405 if (sched_feat(NEXT_BUDDY) &&
5406 cfs_rq->next && entity_eligible(cfs_rq, cfs_rq->next))
5407 return cfs_rq->next;
5408
5409 return pick_eevdf(cfs_rq);
5410 }
Linux 内核调度器中用于选择下一个任务的核心逻辑之一,它在 pick_next_entity
函数中实现。虽然 CFS 调度器传统上依赖于 虚拟运行时间(vruntime
) 来实现公平调度,但从 Linux 6.6 开始,内核引入了新的调度算法 EEVDF(Earliest Eligible Virtual Deadline First),这使得任务选择的逻辑更加复杂且高效。
1. 函数背景
pick_next_entity
的作用是从当前调度队列(cfs_rq
)中选择下一个调度实体(sched_entity
)。这个函数的核心目标是:
- 公平性:确保任务之间的时间分配公平。
- 效率:尽可能减少延迟和上下文切换开销。
- 缓存局部性:优先选择最近运行过的任务以提高性能。
在 Linux 6.6 中,调度器不再仅仅依赖 vruntime
,而是结合了 EEVDF 和其他优化策略(如 NEXT_BUDDY
)来选择任务。
2. 主要逻辑流程
(1) NEXT_BUDDY 优化
if (sched_feat(NEXT_BUDDY) &&cfs_rq->next && entity_eligible(cfs_rq, cfs_rq->next))return cfs_rq->next;
NEXT_BUDDY
功能:NEXT_BUDDY
是一个调度特性(feature),旨在通过“伙伴任务”机制减少延迟。- 如果启用了
NEXT_BUDDY
,并且cfs_rq->next
存在一个有效的“伙伴任务”,则直接返回该任务。
entity_eligible
检查:- 确保“伙伴任务”符合调度条件(例如,它必须是可运行的)。
- 优点:
- 这种优化可以显著减少延迟,尤其是在某些实时性要求较高的场景中。
- 它不会影响公平性,因为 EEVDF 仍然会在后续步骤中处理公平性问题。
(2) EEVDF 调度算法
return pick_eevdf(cfs_rq);
- 如果没有启用
NEXT_BUDDY
或者没有合适的“伙伴任务”,则调用pick_eevdf
函数选择下一个任务。 - EEVDF 的核心思想:
- EEVDF 是一种基于虚拟截止时间(virtual deadline)的调度算法。
- 每个任务都有一个虚拟截止时间,调度器会优先选择最早到达虚拟截止时间的任务。
- 这种算法可以更好地平衡公平性和延迟,尤其是在高负载或多任务场景中。
3. EEVDF 的工作原理
EEVDF 是 Linux 6.6 中引入的新调度算法,相比传统的 vruntime
排序,EEVDF 提供了更高效的调度决策。以下是其核心概念:
(1) 虚拟截止时间
每个任务的虚拟截止时间(deadline
)由以下公式计算:
deadline = vruntime + period
vruntime
:任务的虚拟运行时间。period
:一个固定的时间间隔,表示任务的目标运行周期。
调度器会选择虚拟截止时间最小的任务作为下一个运行任务。
(2) 与 vruntime
的区别
- 在传统的 CFS 中,任务按照
vruntime
排序,可能导致高优先级任务被低优先级任务抢占。 - 在 EEVDF 中,任务不仅考虑
vruntime
,还考虑虚拟截止时间,从而更好地平衡公平性和延迟。
二.EEVDF(Earliest Eligible Virtual Deadline First) 调度算法的核心逻辑
/*
904 * Earliest Eligible Virtual Deadline First
905 *
906 * In order to provide latency guarantees for different request sizes
907 * EEVDF selects the best runnable task from two criteria:
908 *
909 * 1) the task must be eligible (must be owed service)
910 *
911 * 2) from those tasks that meet 1), we select the one
912 * with the earliest virtual deadline.
913 *
914 * We can do this in O(log n) time due to an augmented RB-tree. The
915 * tree keeps the entries sorted on deadline, but also functions as a
916 * heap based on the vruntime by keeping:
917 *
918 * se->min_vruntime = min(se->vruntime, se->{left,right}->min_vruntime)
919 *
920 * Which allows tree pruning through eligibility.
921 */
922 static struct sched_entity *pick_eevdf(struct cfs_rq *cfs_rq)
923 {
924 struct rb_node *node = cfs_rq->tasks_timeline.rb_root.rb_node;
925 struct sched_entity *se = __pick_first_entity(cfs_rq);
926 struct sched_entity *curr = cfs_rq->curr;
927 struct sched_entity *best = NULL;
928
929 /*
930 * We can safely skip eligibility check if there is only one entity
931 * in this cfs_rq, saving some cycles.
932 */
933 if (cfs_rq->nr_running == 1)
934 return curr && curr->on_rq ? curr : se;
935
936 if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr)))
937 curr = NULL;
938
939 /*
940 * Once selected, run a task until it either becomes non-eligible or
941 * until it gets a new slice. See the HACK in set_next_entity().
942 */
943 if (sched_feat(RUN_TO_PARITY) && curr && curr->vlag == curr->deadline)
944 return curr;
945
946 /* Pick the leftmost entity if it's eligible */
947 if (se && entity_eligible(cfs_rq, se)) {
948 best = se;
949 goto found;
950 }
951
952 /* Heap search for the EEVD entity */
953 while (node) {
954 struct rb_node *left = node->rb_left;
955
956 /*
957 * Eligible entities in left subtree are always better
958 * choices, since they have earlier deadlines.
959 */
960 if (left && vruntime_eligible(cfs_rq,
961 __node_2_se(left)->min_vruntime)) {
962 node = left;
963 continue;
964 }
965
966 se = __node_2_se(node);
967
968 /*
969 * The left subtree either is empty or has no eligible
970 * entity, so check the current node since it is the one
971 * with earliest deadline that might be eligible.
972 */
973 if (entity_eligible(cfs_rq, se)) {
974 best = se;
975 break;
976 }
977
978 node = node->rb_right;
979 }
980 found:
981 if (!best || (curr && entity_before(curr, best)))
982 best = curr;
983
984 return best;
985 }
1. 函数背景
pick_eevdf
的目标是从当前调度队列(cfs_rq
)中选择一个符合条件的任务。它的选择逻辑基于以下两个条件:
- 任务必须是“可选的”(eligible):
- 任务需要满足一定的服务欠账条件(即它需要被分配 CPU 时间)。
- 从符合条件的任务中,选择虚拟截止时间最早的任务:
- 虚拟截止时间(
deadline
)越早,任务优先级越高。
- 虚拟截止时间(
2. 主要逻辑流程
(1) 单任务优化
if (cfs_rq->nr_running == 1)return curr && curr->on_rq ? curr : se;
- 如果当前调度队列中只有一个任务,则直接返回该任务。
- 如果当前任务(
curr
)在运行队列上(on_rq
),则返回当前任务;否则返回红黑树中最左边的任务(se
)。 - 这种优化避免了不必要的复杂逻辑,提高了效率。
(2) 当前任务的检查
if (curr && (!curr->on_rq || !entity_eligible(cfs_rq, curr)))curr = NULL;
- 检查当前任务是否仍然在运行队列上(
on_rq
)并且是否符合“可选性”条件(entity_eligible
)。 - 如果当前任务不符合条件,则将其置为
NULL
。
(3) RUN_TO_PARITY 特性
if (sched_feat(RUN_TO_PARITY) && curr && curr->vlag == curr->deadline)return curr;
- 如果启用了
RUN_TO_PARITY
特性,并且当前任务的虚拟标记(vlag
)等于其虚拟截止时间(deadline
),则直接返回当前任务。 RUN_TO_PARITY
的作用:- 确保任务在其虚拟截止时间到达之前尽可能多地运行,从而减少上下文切换开销。
(4) 左子树优先选择
if (se && entity_eligible(cfs_rq, se)) {best = se;goto found;
}
- 检查红黑树中最左边的任务(
se
)是否符合条件。 - 如果符合条件,则直接选择该任务,并跳转到
found
标签。
(5) 堆搜索(Heap Search)
while (node) {struct rb_node *left = node->rb_left;if (left && vruntime_eligible(cfs_rq,__node_2_se(left)->min_vruntime)) {node = left;continue;}se = __node_2_se(node);if (entity_eligible(cfs_rq, se)) {best = se;break;}node = node->rb_right;
}
- 左子树优先:
- 如果左子树存在并且其中的任务符合条件,则继续向左遍历。
- 左子树中的任务总是具有更早的虚拟截止时间,因此优先考虑。
- 当前节点检查:
- 如果左子树不包含符合条件的任务,则检查当前节点的任务。
- 如果当前节点的任务符合条件,则选择该任务并退出循环。
- 右子树遍历:
- 如果当前节点的任务不符合条件,则继续向右遍历。
(6) 最终选择
if (!best || (curr && entity_before(curr, best)))best = curr;
- 如果没有找到符合条件的任务,或者当前任务比找到的任务更优(
entity_before
),则选择当前任务。 entity_before
的作用:- 比较两个任务的虚拟运行时间(
vruntime
),选择较小的那个。
- 比较两个任务的虚拟运行时间(
3. 关键点解析
(1) 可选性(Eligibility)
- 定义:
- 一个任务被认为是“可选的”,当它的虚拟运行时间(
vruntime
)小于或等于调度队列的最小虚拟运行时间(min_vruntime
)。
- 一个任务被认为是“可选的”,当它的虚拟运行时间(
- 作用:
- 确保任务不会因为不公平的调度而被饿死。
(2) 虚拟截止时间(Deadline)
- 计算公式:
deadline = vruntime + period
period
是一个固定的时间间隔,表示任务的目标运行周期。
(3) 红黑树的双重角色
- 按虚拟截止时间排序:
- 红黑树按照虚拟截止时间(
deadline
)对任务进行排序,确保可以快速找到最早的任务。
- 红黑树按照虚拟截止时间(
- 按虚拟运行时间堆化:
- 每个节点维护一个
min_vruntime
,表示该子树中所有任务的最小虚拟运行时间。 - 这种设计允许通过剪枝(pruning)快速排除不符合条件的任务。
- 每个节点维护一个
4. 示例场景
假设一个系统中有以下任务:
- 任务 A:
vruntime = 100
,deadline = 150
。 - 任务 B:
vruntime = 90
,deadline = 140
。 - 任务 C:
vruntime = 80
,deadline = 160
。
(1) 单任务优化
- 如果调度队列中只有任务 A,则直接返回任务 A。
(2) 当前任务检查
- 如果当前任务是任务 B,但任务 B 不符合条件(例如,不在运行队列上),则将其置为
NULL
。
(3) 左子树优先选择
- 如果红黑树中最左边的任务是任务 B,并且任务 B 符合条件,则直接选择任务 B。
(4) 堆搜索
- 遍历红黑树,首先检查左子树中的任务。
- 如果左子树中没有符合条件的任务,则检查当前节点的任务。
- 如果当前节点的任务符合条件,则选择该任务。
5. 总结
pick_eevdf
的核心逻辑可以分为以下几个步骤:
- 单任务优化:如果只有一个任务,则直接返回。
- 当前任务检查:确保当前任务符合条件。
- 左子树优先选择:优先选择红黑树中最左边的任务。
- 堆搜索:通过红黑树的剪枝机制快速找到符合条件的任务。
- 最终选择:比较当前任务和找到的任务,选择最优的任务。
三.EEVDF 的实现细节
int entity_eligible(struct cfs_rq *cfs_rq, struct sched_entity *se)
786 {
787 return vruntime_eligible(cfs_rq, se->vruntime);
788 }/*
753 * Entity is eligible once it received less service than it ought to have,
754 * eg. lag >= 0.
755 *
756 * lag_i = S - s_i = w_i*(V - v_i)
757 *
758 * lag_i >= 0 -> V >= v_i
759 *
760 * \Sum (v_i - v)*w_i
761 * V = ------------------ + v
762 * \Sum w_i
763 *
764 * lag_i >= 0 -> \Sum (v_i - v)*w_i >= (v_i - v)*(\Sum w_i)
765 *
766 * Note: using 'avg_vruntime() > se->vruntime' is inacurate due
767 * to the loss in precision caused by the division.
768 */
769 static int vruntime_eligible(struct cfs_rq *cfs_rq, u64 vruntime)
770 {
771 struct sched_entity *curr = cfs_rq->curr;
772 s64 avg = cfs_rq->avg_vruntime;
773 long load = cfs_rq->avg_load;
774
775 if (curr && curr->on_rq) {
776 unsigned long weight = scale_load_down(curr->load.weight);
777
778 avg += entity_key(cfs_rq, curr) * weight;
779 load += weight;
780 }
781
782 return avg >= (s64)(vruntime - cfs_rq->min_vruntime) * load;
783 }
784
1. 传统 CFS 的问题
在传统的 CFS 调度器中,任务的选择主要依赖于 vruntime
:
- 公平性:通过
vruntime
确保所有任务的 CPU 时间分配是公平的。 - 延迟问题:
- 高负载场景下,长任务可能会被频繁抢占,导致延迟增加。
- 短任务可能会因为长任务的高
vruntime
而等待过久。
这些问题的核心在于 vruntime
只考虑了运行时间,而没有考虑任务的“需求”(如任务的优先级或服务欠账)。
2. EEVDF 的改进
EEVDF 的设计目标是通过引入 虚拟截止时间(deadline
) 和 服务延迟(lag) 的概念来解决上述问题。以下是 EEVDF 的关键特性及其相对于传统 vruntime
的优势:
(1) 引入虚拟截止时间
- 每个任务都有一个虚拟截止时间(
deadline
),计算公式为:
deadline = vruntime + period
period
是一个固定的时间间隔,表示任务的目标运行周期。- 作用:
- 虚拟截止时间决定了任务的优先级,调度器会选择最早到达虚拟截止时间的任务。
- 这种机制确保短任务能够快速得到调度,从而减少延迟。
(2) 引入服务延迟(Eligibility)
- 服务欠账的定义:
- 任务的服务欠账(
lag
)表示它实际获得的服务量与其应得服务量之间的差距。 - 公式为:
- 任务的服务欠账(
lag_i = S - s_i = w_i * (V - v_i)
S
:系统总服务量。s_i
:任务i
实际获得的服务量。w_i
:任务i
的权重。V
:系统的平均虚拟运行时间。v_i
:任务i
的虚拟运行时间。
- 条件:
- 如果
lag_i >= 0
,则任务被认为是“可选的”(eligible)。 - 这意味着任务需要更多的服务,调度器会优先选择它。
- 如果
- 作用:
- 通过服务欠账的计算,确保任务不会因为不公平的调度而被饿死。
- 这种机制使得 EEVDF 在高负载场景下表现更好。
(3) 平均虚拟运行时间(avg_vruntime
)
- 在
vruntime_eligible
函数中,计算了系统的平均虚拟运行时间(avg_vruntime
):
s64 avg = cfs_rq->avg_vruntime;
773 long load = cfs_rq->avg_load;
774
775 if (curr && curr->on_rq) {
776 unsigned long weight = scale_load_down(curr->load.weight);
777
778 avg += entity_key(cfs_rq, curr) * weight;
779 load += weight;
780 }
avg_vruntime
表示所有任务的加权平均虚拟运行时间。- 它用于判断某个任务是否符合“可选性”条件。
- 作用:
avg_vruntime
提供了一个全局视角,避免了传统 CFS 中因单个任务vruntime
偏移而导致的不公平现象。
3. vruntime_eligible
的逻辑解析
static int vruntime_eligible(struct cfs_rq *cfs_rq, u64 vruntime)
{struct sched_entity *curr = cfs_rq->curr;s64 avg = cfs_rq->avg_vruntime;long load = cfs_rq->avg_load;if (curr && curr->on_rq) {unsigned long weight = scale_load_down(curr->load.weight);avg += entity_key(cfs_rq, curr) * weight;load += weight;}return avg >= (s64)(vruntime - cfs_rq->min_vruntime) * load;
}
(1) 计算平均虚拟运行时间
avg_vruntime
是所有任务的加权平均虚拟运行时间。- 如果当前任务(
curr
)在运行队列上,则将其贡献加入到avg_vruntime
和load
中。
(2) 判断可选性
- 最终返回值是一个布尔值,判断以下条件是否成立:
avg >= (vruntime - cfs_rq->min_vruntime) * load
- 如果成立,则任务被认为是“可选的”。
- 这个条件结合了
avg_vruntime
、vruntime
和min_vruntime
,确保任务的选择既公平又高效。
4. EEVDF 的优势总结
相比传统 vruntime
,EEVDF 的改进体现在以下几个方面:
- 更好的延迟控制:
- 通过虚拟截止时间(
deadline
),确保短任务能够快速得到调度。
- 通过虚拟截止时间(
- 更高的公平性:
- 通过服务欠账(
lag
)和平均虚拟运行时间(avg_vruntime
),确保任务不会因为不公平的调度而被饿死。
- 通过服务欠账(
- 更高效的搜索:
- 红黑树既按虚拟截止时间排序,又按虚拟运行时间堆化,支持 O(log n) 时间内找到符合条件的任务。
- 动态适应性:
- 通过加权平均和动态调整,EEVDF 能够更好地适应高负载和多任务场景。
5. 示例对比
假设一个系统中有以下任务:
- 任务 A:
vruntime = 100
,deadline = 150
。 - 任务 B:
vruntime = 90
,deadline = 140
。 - 任务 C:
vruntime = 80
,deadline = 160
。
(1) 传统 CFS
- 传统 CFS 仅根据
vruntime
排序,选择任务 C(vruntime = 80
)。 - 但如果任务 C 是一个长任务,可能会导致短任务(如任务 B)等待过久。
(2) EEVDF
- EEVDF 根据虚拟截止时间选择任务 B(
deadline = 140
)。 - 同时,通过服务欠账的计算,确保任务 A 和任务 C 不会被饿死。
6. 总结
虽然从代码表面看,EEVDF 的实现与传统 CFS 类似,但其核心思想和机制有着本质的不同。EEVDF 通过引入虚拟截止时间和服务欠账的概念,解决了传统 CFS 在高负载和多任务场景下的延迟和公平性问题。
**附加知识
lag_i = S - s_i = w_i * (V - v_i)
1. 公式的理论背景
公式 lag_i = S - s_i = w_i * (V - v_i)
是 EEVDF 的核心理论之一,用于衡量任务的服务欠账(lag
)。以下是公式的分解:
(1) 符号解释
S
:系统总服务量。s_i
:任务i
实际获得的服务量。w_i
:任务i
的权重。V
:系统的平均虚拟运行时间。v_i
:任务i
的虚拟运行时间。
(2) 公式的含义
- 服务欠账(
lag
):- 如果
lag_i >= 0
,表示任务i
获得的服务量少于它应得的服务量,因此它是“可选的”(eligible)。 - 如果
lag_i < 0
,表示任务i
已经获得了足够的服务,暂时不需要被调度。
- 如果
- 加权差值:
- 公式的核心是通过加权差值(
w_i * (V - v_i)
)来计算服务欠账。 - 加权差值考虑了任务的权重(优先级),确保高优先级任务能够更快地获得服务。
- 公式的核心是通过加权差值(
2. 公式在代码中的隐含实现
虽然代码中没有直接使用 lag_i = w_i * (V - v_i)
,但它的逻辑已经通过其他方式体现出来。以下是具体分析:
(1) 平均虚拟运行时间(avg_vruntime
)
s64 avg = cfs_rq->avg_vruntime;
long load = cfs_rq->avg_load;if (curr && curr->on_rq) {unsigned long weight = scale_load_down(curr->load.weight);avg += entity_key(cfs_rq, curr) * weight;load += weight;
}
avg_vruntime
:表示所有任务的加权平均虚拟运行时间(V
)。load
:表示所有任务的总权重。- 通过这两个变量,代码隐含地计算了系统的平均虚拟运行时间(
V
)。
(2) 判断服务欠账
return avg >= (s64)(vruntime - cfs_rq->min_vruntime) * load;
vruntime - cfs_rq->min_vruntime
:- 表示当前任务的虚拟运行时间与调度队列的最小虚拟运行时间之间的差距。
- 这个值可以看作是任务的相对延迟。
avg >= ... * load
:- 这里的
avg
和load
结合起来,实际上是在判断任务的服务欠账是否为正。 - 如果条件成立,则任务被认为是“可选的”。
- 这里的
(3) 隐含的公式变形
通过对代码的分析,我们可以将公式 lag_i = w_i * (V - v_i)
变形为以下形式:
lag_i ≈ load * (avg_vruntime - vruntime)
avg_vruntime
对应公式的V
。vruntime
对应公式的v_i
。load
对应公式的权重部分。
代码中的 avg >= (vruntime - cfs_rq->min_vruntime) * load
实际上是对上述变形公式的实现。
3. 为什么没有直接实现公式?
Linux 内核在实现 EEVDF 时,对公式进行了以下优化和简化:
- 避免浮点运算:
- 内核中尽量避免使用浮点运算,以提高效率。
- 因此,公式中的权重(
w_i
)和虚拟运行时间(v_i
)都通过整数运算实现。
- 减少计算复杂度:
- 直接计算
lag_i
需要遍历所有任务,计算其权重和服务量。这会显著增加计算开销。 - 通过维护
avg_vruntime
和load
,内核能够在 O(1) 时间内判断任务的可选性。
- 直接计算
- 动态调整:
avg_vruntime
和load
是动态更新的,能够快速适应系统状态的变化。
4. 示例对比
假设一个系统中有以下任务:
- 任务 A:
vruntime = 100
,权重w = 1
。 - 任务 B:
vruntime = 90
,权重w = 2
。 - 系统的
avg_vruntime = 95
,load = 3
。
(1) 计算服务欠账
根据公式 lag_i = w_i * (V - v_i)
:
- 任务 A:
lag_A = 1 * (95 - 100) = -5
- 任务 A 的服务欠账为负,表示它已经获得了足够的服务。
任务 B:
lag_B = 2 * (95 - 90) = 10
- 任务 B 的服务欠账为正,表示它需要更多的服务。
(2) 代码中的实现
在代码中,判断条件为:
avg >= (s64)(vruntime - cfs_rq->min_vruntime) * load
任务 A:
95 >= (100 - 95) * 3 => 95 >= 15
- 条件成立,任务 A 符合条件。
任务 B:
95 >= (90 - 95) * 3 => 95 >= -15
- 条件成立,任务 B 符合条件。
综合下来任务B可能更加符合