当前位置: 首页 > news >正文

关于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 的核心逻辑可以分为以下几个步骤:

  1. 检查是否有可运行任务
    • 如果没有任务,进入空闲状态或尝试负载均衡。
  2. 多层级调度组的支持
    • 遍历调度组,选择最合适的任务。
    • 最小化调度组的更新,提高效率。
  3. 简单路径
    • 直接从运行队列中选择下一个任务。
  4. 返回结果
    • 更新任务列表、定时器和调度状态。
    • 返回选定的任务。
  5. 空闲状态处理
    • 尝试负载均衡。
    • 更新空闲队列的时间统计。

三.研究一下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 的主要功能是:

  1. 更新当前任务的运行时统计信息。
  2. 计算并更新当前任务的虚拟运行时间(vruntime)。
  3. 维护调度队列(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 调度器的核心机制,用于实现公平调度。它的主要作用包括:

  1. 公平性:通过虚拟运行时间,所有任务都能公平地分配到 CPU 时间。
  2. 任务选择:CFS 使用红黑树维护所有可运行任务,按照 vruntime 排序。vruntime 最小的任务会被优先调度。
  3. 动态调整:每次调用 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 会被用于:

  1. 在红黑树中维护任务的排序。
  2. 决定下一个任务的选择(pick_next_entity)。
  3. 确保调度过程的公平性和高效性。

有一个疑问:

如果有当前任务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 调度器通过以下机制确保公平性:

  1. min_vruntime:维护一个全局的最小虚拟运行时间,确保新任务和唤醒任务能够公平参与调度。
  2. 红黑树排序:任务按照 vruntime 排序,当前任务的 vruntime 增大后会自动让出优先权。
  3. 等待任务的补偿:未运行任务的 vruntime 相对较小,自然会在调度中获得优先权。
http://www.dtcms.com/a/393833.html

相关文章:

  • 【算法笔记】链表相关的题目
  • Netty从0到1系列之Recycler对象池技术【3】
  • 网页开发入门:CSS与JS基础及BS/CS架构解析
  • css单位换算及适配
  • Java制作双脑同步 Hemi-Sync 音频
  • webrtc弱网-ProbeBitrateEstimator类源码分析与算法原理
  • 在OpenHarmony上适配图形显示【4】——rk3568_4.0r_mesa3d适配
  • 嵌入式(3)——RTC实时时钟
  • 内核模块组成和裁剪参考表
  • 140-understanding_the_armv8.x_and_armv9.x_extensions_guide
  • 【序列晋升】40 Spring Data R2DBC 轻量异步架构下的数据访问最佳实践
  • TGRS | 视觉语言模型 | 语言感知领域泛化实现高光谱跨场景分类, 代码开源!
  • Oracle / MySQL / MariaDB / SQL Server 常用连接与基础查询(Linux操作系统上)
  • 将 Jupyter Notebook 转换为 PDF
  • torchvision 编译安装 nano
  • 华为昇腾 910 到 950 系列 NPU 深度解析
  • 设计模式---门面模式
  • SQL Server从入门到项目实践(超值版)读书笔记 26
  • Datawhale学习笔记——深度语义匹配模型DSSM详解、实战与FAQ
  • 一文了解瑞萨MCU常用的芯片封装类型
  • LeetCode:44.二叉搜索树中第K小的元素
  • 初学者如何系统性地学习Linux?
  • LeetCode:43.验证二叉搜索树
  • [学习log] OT/ICS工业控制系统渗透测试
  • 六边形箱图 (Hexbin Plot):使用 Matplotlib 处理大规模散点数据
  • LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(2——Parser解析html模块)
  • 电脑能ping开发板,开发板不能ping电脑的解决方法:
  • git 覆盖:检出特定分支的文件到当前分支
  • CentOS 8.5.2.111部署Zabbix6.0
  • 【Elasticsearch面试精讲 Day 20】集群监控与性能评估