2. 调度实体与调度类:struct sched_entity 结构体、调度类(sched_class)接口、stop_sched_class与dl_sched_class

在 Linux 内核的 CFS 调度框架中,调度实体 (Scheduling Entity) 和 调度类 (Scheduling Class) 是两个核心抽象。调度实体是内核可调度的最小单元,而调度类则定义了不同调度策略的通用接口。理解这两者的关系,是掌握进程调度调优的基础。

2.1 调度实体:struct sched_entity

struct sched_entity 是 CFS 调度器用于管理可运行进程(或进程组)的核心数据结构。它不直接对应一个进程的 task_struct,而是嵌入在其中,专门供 CFS 使用。

关键设计思想: CFS 不直接调度进程,而是调度“调度实体”。这使得 CFS 可以统一调度单个进程和进程组(通过组调度),实现了公平分配 CPU 时间的基础。

该结构体主要包含以下关键字段:

字段名 类型 作用描述
load struct load_weight 调度实体的权重,决定了其获得 CPU 时间的比例。权重越高,获得的 CPU 时间越多。
run_node struct rb_node 用于将调度实体插入 CFS 运行队列的红黑树节点。CFS 通过红黑树按 vruntime 排序。
on_rq unsigned int 标志位,指示该调度实体当前是否在运行队列上。
vruntime u64 虚拟运行时间。这是 CFS 调度的核心指标,用于记录实体在“理想多核处理器”上运行的时间。CFS 总是选择 vruntime 最小的实体来运行。
exec_start u64 当前实体开始执行的时间戳,用于计算本次运行的时间片。
sum_exec_runtime u64 该实体在 CPU 上实际执行的总时间(纳秒级)。
prev_sum_exec_runtime u64 上次被切换出 CPU 时的 sum_exec_runtime 快照,用于计算本次运行时间。
depth int 在组调度层级中的深度。根调度实体深度为 0,其子实体深度为 1,以此类推。
parent struct cfs_rq * 指向该实体所属的 CFS 运行队列(cfs_rq)的指针。
my_q struct cfs_rq * 如果该实体是一个任务组(task group),则 my_q 指向该组自己的 CFS 运行队列。
调优视角: vruntime 是 CFS 调度的“罗盘”。在调优时,理解 vruntime 的计算方式(受 nice 值和权重影响)至关重要。通过调整进程的 nice 值,可以间接改变其调度实体的权重,从而影响 CPU 时间分配。

2.2 调度类接口:sched_class

struct sched_class 是 Linux 内核调度框架的“多态”接口。它定义了一组函数指针,每个调度类(如 CFS、实时调度类、空闲调度类)都必须实现这些接口。调度核心代码通过调用这些统一的接口来执行调度操作,而无需关心底层具体的调度算法。

以下是 sched_class 中最重要的几个接口方法:

  • enqueue_task:将调度实体加入运行队列。当进程变为可运行状态时调用。
  • dequeue_task:将调度实体从运行队列中移除。当进程阻塞或退出时调用。
  • yield_task:进程主动让出 CPU。
  • check_preempt_curr:检查新唤醒的进程是否可以抢占当前正在运行的进程。
  • pick_next_task:从运行队列中选择下一个要运行的调度实体。这是调度器的核心决策点。
  • put_prev_task:将当前正在运行的进程放回运行队列。
  • set_curr_task:当进程被选中运行时,更新调度器的内部状态。
  • task_tick:周期性时钟中断处理函数。CFS 在此函数中更新 vruntime 并检查是否需要调度。
  • update_curr:更新当前运行进程的运行时统计信息。
架构意义: 调度类接口的设计使得 Linux 内核可以同时支持多种调度策略。每个 CPU 核心都有一个运行队列,该队列由多个调度类的运行队列组成。调度核心按照优先级顺序遍历调度类(Stop > Deadline > Real-time > CFS > Idle),直到找到有可运行实体的调度类。

下图展示了调度类接口在调度决策流程中的核心位置:

调度类接口与调度决策流程 核心调度器 (__schedule) sched_class->pick_next_task() 按优先级遍历调度类链表 Stop → Deadline → Real-time → CFS → Idle 返回选中的调度实体 接口多态性 每个调度类独立 实现这些接口 核心代码无需修改

2.3 stop_sched_class 与 dl_sched_class

在 Linux 内核中,调度类按照优先级从高到低排列。其中 stop_sched_class 和 dl_sched_class 是两个特殊且优先级极高的调度类。

2.3.1 stop_sched_class(停止调度类)

stop_sched_class 是系统中优先级最高的调度类。它用于执行一些需要独占 CPU 且不能被中断的特殊任务,例如 migration 线程(用于在 CPU 之间迁移进程)和 stop 线程。

  • 特性: 该调度类中的线程一旦被选中,将一直运行直到主动完成或放弃 CPU。它不会被任何其他调度类抢占。
  • 典型用途: CPU 热插拔、进程迁移、内核调试中的单步执行。
  • 调优影响: 普通调优场景下无需直接干预。但理解其存在有助于解释为什么在某些极端负载下,某些 CPU 核心会短暂“卡住”——这通常是 stop 线程在执行关键操作。

2.3.2 dl_sched_class(Deadline 调度类)

dl_sched_class 实现了 SCHED_DEADLINE 策略,基于 Earliest Deadline First (EDF) 算法。它为实时任务提供了比普通实时调度类(rt_sched_class)更精确的时序保证。

特性 描述
调度算法 Earliest Deadline First (EDF) + CBS (Constant Bandwidth Server)
核心参数 sched_runtime(运行时间)、sched_deadline(截止时间)、sched_period(周期)
优先级 高于 rt_sched_class,低于 stop_sched_class
适用场景 音视频编解码、工业控制、自动驾驶等对延迟有严格上限要求的实时任务
带宽隔离 CBS 机制确保每个 Deadline 任务不会消耗超过其声明的 CPU 带宽,保护其他任务
调优警告: 使用 dl_sched_class 需要非常谨慎。如果配置了过多的 Deadline 任务或设置了过大的 sched_runtime,可能会导致系统总 CPU 带宽超过 100%,从而造成系统不稳定甚至死锁。内核提供了 sched_dl_runtime_us 和 sched_dl_period_us 等 cgroup 参数来限制总带宽。

下图展示了 Linux 内核中所有调度类的优先级层次结构:

Linux 内核调度类优先级层次 stop_sched_class (最高优先级) dl_sched_class (SCHED_DEADLINE) rt_sched_class (SCHED_FIFO / SCHED_RR) fair_sched_class (SCHED_NORMAL / SCHED_BATCH) idle_sched_class (最低优先级) 调度优先级 从上到下 优先级递减 核心遍历时 按此顺序查找
实践要点: 在系统调优时,可以通过 /proc/sched_debug 或 ftrace 查看当前系统中各个调度类的运行情况。如果发现 stop 或 dl 调度类占用了过多的 CPU 时间,通常意味着系统存在异常(如频繁的 CPU 热插拔操作或 Deadline 任务配置不当),需要进一步排查。