关于CFS队列pick_next_task_fair选取下一个任务的分析
一.pick_next_task_fair
1. 函数背景
pick_next_task_fair
是 CFS 调度器中用于选择下一个任务的关键函数。它的目标是:
- 从当前运行队列中选择一个任务。
- 确保调度过程公平且高效。
- 在多层级调度组(group scheduling)的情况下,处理复杂的调度结构。
struct task_struct *
8376 pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
8377 {
8378 struct cfs_rq *cfs_rq = &rq->cfs;
8379 struct sched_entity *se = NULL;
8380 struct task_struct *p = NULL;
8381 int new_tasks;
8382 bool repick = false;
8383
8384 again:
8385 if (!sched_fair_runnable(rq))
8386 goto idle;
8387
8388 #ifdef CONFIG_FAIR_GROUP_SCHED
8389 if (!prev || prev->sched_class != &fair_sched_class)
8390 goto simple;
8391
8392 /*
8393 * Because of the set_next_buddy() in dequeue_task_fair() it is rather
8394 * likely that a next task is from the same cgroup as the current.
8395 *
8396 * Therefore attempt to avoid putting and setting the entire cgroup
8397 * hierarchy, only change the part that actually changes.
8398 */
8399
8400 do {
8401 struct sched_entity *curr = cfs_rq->curr;
8402
8403 /*
8404 * Since we got here without doing put_prev_entity() we also
8405 * have to consider cfs_rq->curr. If it is still a runnable
8406 * entity, update_curr() will update its vruntime, otherwise
8407 * forget we've ever seen it.
8408 */
8409 if (curr) {
8410 if (curr->on_rq)
8411 update_curr(cfs_rq);
8412 else
8413 curr = NULL;
8414
8415 /*
8416 * This call to check_cfs_rq_runtime() will do the
8417 * throttle and dequeue its entity in the parent(s).
8418 * Therefore the nr_running test will indeed
8419 * be correct.
8420 */
8421 if (unlikely(check_cfs_rq_runtime(cfs_rq))) {
8422 cfs_rq = &rq->cfs;
8423
8424 if (!cfs_rq->nr_running)
8425 goto idle;
8426
8427 goto simple;
8428 }
8429 }
8430
8431 se = pick_next_entity(cfs_rq, curr);
8432 cfs_rq = group_cfs_rq(se);
8433 } while (cfs_rq);
8434
8435 p = task_of(se);
8436 trace_android_rvh_replace_next_task_fair(rq, &p, &se, &repick, false, prev);
8437 /*
8438 * Since we haven't yet done put_prev_entity and if the selected task
8439 * is a different task than we started out with, try and touch the
8440 * least amount of cfs_rqs.
8441 */
8442 if (prev != p) {
8443 struct sched_entity *pse = &prev->se;
8444
8445 while (!(cfs_rq = is_same_group(se, pse))) {
8446 int se_depth = se->depth;
8447 int pse_depth = pse->depth;
8448
8449 if (se_depth <= pse_depth) {
8450 put_prev_entity(cfs_rq_of(pse), pse);
8451 pse = parent_entity(pse);
8452 }
8453 if (se_depth >= pse_depth) {
8454 set_next_entity(cfs_rq_of(se), se);
8455 se = parent_entity(se);
8456 }
8457 }
8458
8459 put_prev_entity(cfs_rq, pse);
8460 set_next_entity(cfs_rq, se);
8461 }
8462
8463 goto done;
8464 simple:
8465 #endif
8466 if (prev)
8467 put_prev_task(rq, prev);
8468
8469 trace_android_rvh_replace_next_task_fair(rq, &p, &se, &repick, true, prev);
8470 if (repick)
8471 goto done;
8472
8473 do {
8474 se = pick_next_entity(cfs_rq, NULL);
8475 set_next_entity(cfs_rq, se);
8476 cfs_rq = group_cfs_rq(se);
8477 } while (cfs_rq);
8478
8479 p = task_of(se);
8480
8481 done: __maybe_unused;
8482 #ifdef CONFIG_SMP
8483 /*
8484 * Move the next running task to the front of
8485 * the list, so our cfs_tasks list becomes MRU
8486 * one.
8487 */
8488 list_move(&p->se.group_node, &rq->cfs_tasks);
8489 #endif
8490
8491 if (hrtick_enabled_fair(rq))
8492 hrtick_start_fair(rq, p);
8493
8494 update_misfit_status(p, rq);
8495 sched_fair_update_stop_tick(rq, p);
8496
8497 return p;
8498
8499 idle:
8500 if (!rf)
8501 return NULL;
8502
8503 new_tasks = newidle_balance(rq, rf);
8504
8505 /*
8506 * Because newidle_balance() releases (and re-acquires) rq->lock, it is
8507 * possible for any higher priority task to appear. In that case we
8508 * must re-start the pick_next_entity() loop.
8509 */
8510 if (new_tasks < 0)
8511 return RETRY_TASK;
8512
8513 if (new_tasks > 0)
8514 goto again;
8515
8516 /*
8517 * rq is about to be idle, check if we need to update the
8518 * lost_idle_time of clock_pelt
8519 */
8520 update_idle_rq_clock_pelt(rq);
8521
8522 return NULL;
8523 }
2.更新当前任务的虚拟运行时间
if (curr) {
if (curr->on_rq)
update_curr(cfs_rq);
else
curr = NULL;
}
- 如果当前任务仍在运行队列上,调用
update_curr
更新其虚拟运行时间(vruntime
)。- 如果当前任务已不在运行队列上,则清空
curr
。
3.逐层选择下一个任务
// 逐层选择下一个任务
do {
se = pick_next_entity(cfs_rq, curr);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
- 使用
pick_next_entity
从当前调度组中选择下一个调度实体(sched_entity
)。- 如果该调度实体属于更高级别的调度组,则继续向上遍历,直到找到最终的任务。
4.切换上下文:最小化调度组的更新
if (prev != p) {
...
while (!(cfs_rq = is_same_group(se, pse))) {
...
}
put_prev_entity(cfs_rq, pse);
set_next_entity(cfs_rq, se);
}
- 如果新任务与前一个任务不同,则尽量只更新受影响的调度组,避免不必要的全局更新。
二.总结
pick_next_task_fair
的核心逻辑可以分为以下几个步骤:
- 检查是否有可运行任务:
- 如果没有任务,进入空闲状态或尝试负载均衡。
- 多层级调度组的支持:
- 遍历调度组,选择最合适的任务。
- 最小化调度组的更新,提高效率。
- 简单路径:
- 直接从运行队列中选择下一个任务。
- 返回结果:
- 更新任务列表、定时器和调度状态。
- 返回选定的任务。
- 空闲状态处理:
- 尝试负载均衡。
- 更新空闲队列的时间统计。
三.研究一下update_curr这个函数作用
if (curr) {if (curr->on_rq)update_curr(cfs_rq);
首先先看一下update_curr函数实现:
/*
1160 * Update the current task's runtime statistics.
1161 */
1162 static void update_curr(struct cfs_rq *cfs_rq)
1163 {
1164 struct sched_entity *curr = cfs_rq->curr;
1165 u64 now = rq_clock_task(rq_of(cfs_rq));
1166 u64 delta_exec;
1167
1168 if (unlikely(!curr))
1169 return;
1170
1171 delta_exec = now - curr->exec_start;
1172 if (unlikely((s64)delta_exec <= 0))
1173 return;
1174
1175 curr->exec_start = now;
1176
1177 if (schedstat_enabled()) {
1178 struct sched_statistics *stats;
1179
1180 stats = __schedstats_from_se(curr);
1181 __schedstat_set(stats->exec_max,
1182 max(delta_exec, stats->exec_max));
1183 }
1184
1185 curr->sum_exec_runtime += delta_exec;
1186 schedstat_add(cfs_rq->exec_clock, delta_exec);
1187
1188 curr->vruntime += calc_delta_fair(delta_exec, curr);
1189 update_deadline(cfs_rq, curr);
1190 update_min_vruntime(cfs_rq);
1191
1192 if (entity_is_task(curr)) {
1193 struct task_struct *curtask = task_of(curr);
1194
1195 trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
1196 cgroup_account_cputime(curtask, delta_exec);
1197 account_group_exec_runtime(curtask, delta_exec);
1198 }
1199
1200 account_cfs_rq_runtime(cfs_rq, delta_exec);
1201 }
1202
update_curr
函数的主要作用是更新当前正在运行任务的运行时统计信息,尤其是 虚拟运行时间(vruntime
)。这个vruntime
是 CFS 调度器的核心概念之一,用于实现公平调度。
1. update_curr
的核心功能
update_curr
的主要功能是:
- 更新当前任务的运行时统计信息。
- 计算并更新当前任务的虚拟运行时间(
vruntime
)。 - 维护调度队列(
cfs_rq
)的最小虚拟运行时间(min_vruntime
),以确保公平性。
这些信息将用于后续的调度决策,比如从红黑树中选择下一个任务。
2. 代码逻辑解析
(1) 获取当前时间和计算执行时间增量
u64 now = rq_clock_task(rq_of(cfs_rq));
// 当前任务curr实际执行时间
u64 delta_exec = now - curr->exec_start;
now
:获取当前时间戳。delta_exec
:计算自上次更新以来的执行时间增量(即当前任务已经运行了多长时间)。- 如果
delta_exec <= 0
,说明没有新的执行时间需要更新,直接返回。
(2) 更新执行开始时间
curr->exec_start = now;
- 将当前任务的执行开始时间更新为最新的时间戳,以便下一次调用时正确计算增量。
(3) 更新运行时统计信息
curr->sum_exec_runtime += delta_exec;
schedstat_add(cfs_rq->exec_clock, delta_exec);
sum_exec_runtime
:累加当前任务的总运行时间。exec_clock
:累加调度队列的总运行时间。
(4) 更新虚拟运行时间
curr->vruntime += calc_delta_fair(delta_exec, curr);
calc_delta_fair
:根据任务的权重(weight
)和调度策略,计算虚拟运行时间的增量。- 权重反映了任务的优先级或重要性。高权重的任务会获得更多的 CPU 时间。
vruntime
:累加计算得到的虚拟运行时间增量。
3. 虚拟运行时间的作用
虚拟运行时间(vruntime
)是 CFS 调度器的核心机制,用于实现公平调度。它的主要作用包括:
- 公平性:通过虚拟运行时间,所有任务都能公平地分配到 CPU 时间。
- 任务选择:CFS 使用红黑树维护所有可运行任务,按照
vruntime
排序。vruntime
最小的任务会被优先调度。 - 动态调整:每次调用
update_curr
都会更新当前任务的vruntime
,从而影响其在红黑树中的位置。
4. 红黑树上的比较
CFS 调度器使用红黑树来管理所有可运行任务,任务的排序依据是它们的虚拟运行时间(vruntime
)。以下是与红黑树相关的逻辑:
(1) 插入新任务
当一个新任务被插入到红黑树中时,调度器会根据其 vruntime
决定其位置:
vruntime
较小的任务会被放在树的左侧,表示它们更早应该被调度。vruntime
较大的任务会被放在树的右侧,表示它们稍后才会被调度。
(2) 选择下一个任务
在 pick_next_entity
函数中,调度器会选择红黑树中最左边的任务(即 vruntime
最小的任务)作为下一个运行任务:
se = __pick_first_entity(cfs_rq);
- 这个任务通常是最需要 CPU 时间的任务。
(3) 当前任务的重新插入
如果当前任务仍然在运行队列上(on_rq
),调度器会在更新其 vruntime
后将其重新插入红黑树:
- 如果
vruntime
增加较多,任务可能会被移动到树的右侧。 - 如果
vruntime
增加较少,任务可能仍然保持在树的左侧。
5. 维护最小虚拟运行时间
update_min_vruntime(cfs_rq);
min_vruntime
:调度队列的最小虚拟运行时间,用于确保新任务的vruntime
不会过小。- 新任务的初始
vruntime
通常会被设置为min_vruntime
,以避免新任务“抢占”过多的 CPU 时间。
6. 总结
update_curr
的核心作用是更新当前任务的运行时统计信息和虚拟运行时间(vruntime
)。这个 vruntime
会被用于:
- 在红黑树中维护任务的排序。
- 决定下一个任务的选择(
pick_next_entity
)。 - 确保调度过程的公平性和高效性。
有一个疑问:
如果有当前任务curr在cfs队列中执行,这个时候会调用update_curr更新当前任务的vruntime,那其他任务是否也会同步更新vruntime了?不然不就变成这个当前任务curr的vruntime增大,其他还没有执行的任务vruntime不变么?
确实,
update_curr
只更新当前正在运行任务的vruntime
,而其他未运行的任务的vruntime
并不会同步更新。这看起来可能会导致不公平的情况(即当前任务的
vruntime
增大,而其他任务的vruntime
不变)。但实际上,CFS 调度器通过 最小虚拟运行时间(min_vruntime
) 和 红黑树排序机制 巧妙地解决了这个问题。
1. 为什么只更新当前任务的 vruntime
?
在 CFS 调度器中:
- 当前任务:正在占用 CPU 的任务,其
vruntime
需要根据实际运行时间进行更新。 - 其他任务:这些任务处于等待状态,没有消耗 CPU 时间,因此它们的
vruntime
不需要更新。
这种设计是为了效率和公平性的平衡:
- 效率:如果每次调度都更新所有任务的
vruntime
,会增加不必要的计算开销。 - 公平性:通过维护一个全局的
min_vruntime
,确保新任务或唤醒任务能够公平地参与调度。
2. 其他任务的 vruntime
如何保持公平性?
虽然其他任务的 vruntime
没有直接更新,但它们仍然可以通过以下机制保持公平性:
(1) 最小虚拟运行时间(min_vruntime
)
update_min_vruntime(cfs_rq);
min_vruntime
是调度队列的一个全局变量,表示当前所有任务中最小的vruntime
。- 每次调用
update_curr
时,都会更新min_vruntime
,以反映最新的调度状态。 - 当一个新任务被插入到红黑树中时,它的初始
vruntime
会被设置为min_vruntime
或稍大的值。这确保了新任务不会“抢占”过多的 CPU 时间。
(2) 红黑树的排序机制
CFS 使用红黑树来管理所有可运行任务,任务按照 vruntime
排序:
- 当前任务的
vruntime
更新后,它会在红黑树中重新定位。 - 如果当前任务的
vruntime
增大较多,它会被移动到树的右侧,从而让其他任务有机会被调度。 - 即使其他任务的
vruntime
没有更新,它们仍然会保持在树的左侧,优先于当前任务被调度。
3. 为什么可以容忍 vruntime
的差异?
CFS 的核心思想是 公平性,而不是绝对的实时性。以下几点解释了为什么可以容忍 vruntime
的差异:
(1) vruntime
是相对值
vruntime
是一个相对的时间值,用于比较任务之间的调度优先级。- 只要所有任务的
vruntime
都基于同一个基准(如min_vruntime
),调度器就能保证公平性。
(2) 新任务的初始化
- 新任务的
vruntime
会被初始化为min_vruntime
,以确保它们不会因为延迟插入而失去公平性。 - 这种初始化机制使得新任务能够快速进入调度队列,并与其他任务竞争 CPU 时间。
(3) 等待任务的补偿
- 对于长时间未运行的任务,它们的
vruntime
相对较小,因此在红黑树中会优先被调度。 - 这种机制自然补偿了等待任务的延迟,确保它们不会被饿死。
4. 示例场景
假设一个系统中有三个任务 A、B 和 C,它们的初始 vruntime
分别为:
- A: 100(当前任务)
- B: 50(等待任务)
- C: 75(等待任务)
(1) 当前任务 A 运行一段时间后
- 调用
update_curr
更新 A 的vruntime
,假设增加了 20:- A: 120
- B: 50(不变)
- C: 75(不变)
(2) 插入红黑树
- 更新后的 A 会被重新插入红黑树。
- 由于 A 的
vruntime
增大到 120,它会被移动到树的右侧。 - B 和 C 的
vruntime
较小,仍然保持在树的左侧。
(3) 下一次调度
- 调度器会选择
vruntime
最小的任务(B)作为下一个运行任务。 - B 的
vruntime
开始更新,A 和 C 的vruntime
保持不变。
虽然
update_curr
只更新当前任务的vruntime
,但 CFS 调度器通过以下机制确保公平性:
min_vruntime
:维护一个全局的最小虚拟运行时间,确保新任务和唤醒任务能够公平参与调度。- 红黑树排序:任务按照
vruntime
排序,当前任务的vruntime
增大后会自动让出优先权。- 等待任务的补偿:未运行任务的
vruntime
相对较小,自然会在调度中获得优先权。