4. 调度实体与调度类

在 Linux 内核中,进程调度并非直接操作 task_struct,而是通过两个核心抽象层:调度实体(struct sched_entity) 和 调度类(struct sched_class)。调度实体是调度器眼中的“可调度单元”,调度类则定义了不同调度策略的具体实现。本章将深入剖析这两个结构的设计思想、字段含义以及它们如何构成内核的调度类层次结构。

核心概念:调度实体是“被调度”的对象,调度类是“执行调度”的算法。这种分离设计使得内核可以同时支持多种调度策略(如 CFS、实时、截止时间),并且可以动态扩展。

4.1 调度实体 struct sched_entity

struct sched_entity 是调度器跟踪和管理进程运行时间的基本单位。它被嵌入到 task_struct 中,对于 CFS 调度类,每个进程都有一个对应的调度实体。

4.1.1 核心字段与作用

字段名 类型 作用描述
load struct load_weight 调度实体的权重,决定其占用 CPU 时间的比例
run_node struct rb_node 红黑树节点,用于将实体插入 CFS 运行队列
on_rq unsigned int 标记实体是否在运行队列上(1=在队列,0=已出队)
exec_start u64 当前调度周期开始的时间戳(单位:ns)
sum_exec_runtime u64 实体累计执行的总时间(ns)
vruntime u64 虚拟运行时间,CFS 调度的核心指标
prev_sum_exec_runtime u64 上次切换出 CPU 时的累计执行时间
nr_migrations u64 实体在不同 CPU 之间迁移的次数
statistics struct sched_statistics 调度统计信息(睡眠时间、等待时间等)
depth int 实体在调度组中的嵌套深度
parent struct cfs_rq * 指向所属的 CFS 运行队列
my_q struct cfs_rq * 如果实体代表一个调度组,指向其子运行队列
关键设计:vruntime 是 CFS 调度的“心脏”。内核通过维护红黑树,始终选择 vruntime 最小的实体来运行,从而实现“公平”分配 CPU 时间。

4.1.2 调度实体与 task_struct 的关系

每个进程的 task_struct 中包含多个调度实体,分别对应不同的调度类:

struct task_struct {
    // ... 其他字段 ...
    struct sched_entity    se;          // CFS 调度实体
    struct sched_rt_entity rt;          // 实时调度实体
    struct sched_dl_entity dl;          // 截止时间调度实体
    // ... 其他字段 ...
};

这种设计使得一个进程可以同时被多个调度类管理,但实际运行时只会使用其中一个调度类(由 task_struct->sched_class 指针决定)。

4.1.3 调度实体的生命周期

  • 初始化:在 sched_fork() 中初始化调度实体,设置初始权重和 vruntime。
  • 入队:当进程变为可运行状态时,通过 enqueue_task() 将实体插入对应调度类的运行队列。
  • 调度:调度器从运行队列中选择 vruntime 最小的实体,通过 context_switch() 切换执行。
  • 出队:当进程阻塞或退出时,通过 dequeue_task() 从运行队列中移除。
  • 销毁:在进程结束时,释放调度实体相关的资源。
调度实体生命周期 初始化 入队 调度运行 出队 销毁 说明:调度实体随进程创建而初始化,在进程生命周期中反复入队/出队/调度运行,最终随进程销毁而释放

4.2 调度类 struct sched_class

struct sched_class 是 Linux 内核实现多调度策略的核心抽象。它定义了一组函数指针,每个调度类(如 CFS、实时、截止时间)都需要实现这些接口。调度器通过 task_struct->sched_class 指针调用对应调度类的方法,实现多态调度。

4.2.1 调度类接口定义

struct sched_class {
    const struct sched_class *next;  // 指向下一个优先级更低的调度类

    // 运行队列操作
    void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags);
    void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags);
    void (*yield_task)   (struct rq *rq);
    bool (*yield_to_task)(struct rq *rq, struct task_struct *p, bool preempt);

    // 调度选择
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);
    struct task_struct *(*pick_next_task)(struct rq *rq);

    // 上下文切换
    void (*put_prev_task)(struct rq *rq, struct task_struct *p);
    void (*set_next_task)(struct rq *rq, struct task_struct *p, bool first);

    // 优先级与时间片
    void (*set_curr_task)(struct rq *rq);
    void (*task_tick)(struct rq *rq, struct task_struct *p, int queued);
    void (*task_fork)(struct task_struct *p);
    void (*task_dead)(struct task_struct *p);

    // 负载均衡
    void (*balance)(struct rq *rq, struct task_struct *prev, struct rq_flags *rf);
    int  (*select_task_rq)(struct task_struct *p, int cpu, int flags);

    // 调试与统计
    void (*update_curr)(struct rq *rq);
    unsigned int (*get_rr_interval)(struct rq *rq, struct task_struct *p);
};

4.2.2 核心接口说明

接口 触发时机 典型实现(CFS)
enqueue_task 进程变为可运行状态 将调度实体插入红黑树,更新 vruntime
dequeue_task 进程阻塞或退出 从红黑树移除实体,更新统计信息
pick_next_task 调度器需要选择下一个运行进程 选择红黑树最左节点(vruntime 最小)
put_prev_task 当前进程被切换出 CPU 更新 vruntime,记录执行时间
task_tick 每个时钟 tick 触发 检查是否需要抢占,更新 vruntime
check_preempt_curr 新进程加入运行队列 比较 vruntime,决定是否抢占当前进程
select_task_rq 进程唤醒或迁移时选择目标 CPU 负载均衡算法选择最优 CPU
注意:调度类的 next 指针构成了一个单向链表。当高优先级调度类没有可运行进程时,内核会遍历 next 指针,降级到下一个调度类寻找进程。这是实现调度类层次结构的关键机制。

4.3 调度类层次结构

Linux 内核按照优先级从高到低,将调度类组织成以下层次结构:

stop_sched_class   →   dl_sched_class   →   rt_sched_class   →   fair_sched_class   →   idle_sched_class
(最高优先级)                                                                    (最低优先级)

4.3.1 各调度类职责

调度类 优先级 调度策略 适用场景
stop_sched_class 最高 停止调度类 CPU 停止、热插拔、migration 线程
dl_sched_class 高 截止时间调度(SCHED_DEADLINE) 硬实时任务,要求严格的时间约束
rt_sched_class 中 实时调度(SCHED_FIFO / SCHED_RR) 软实时任务,如音频/视频处理
fair_sched_class 低 完全公平调度(SCHED_NORMAL / SCHED_BATCH) 普通用户进程,交互式与批处理任务
idle_sched_class 最低 空闲调度类 CPU 空闲时运行 idle 进程

4.3.2 调度类选择流程

当内核需要选择下一个运行进程时,遵循以下流程:

  1. 从当前调度类开始:如果当前进程的调度类有可运行进程,优先选择。
  2. 遍历 next 指针:如果当前调度类没有可运行进程,通过 next 指针降级到下一个调度类。
  3. 递归检查:重复步骤 1-2,直到找到可运行进程或到达 idle_sched_class。
  4. 最终保障:如果所有调度类都没有可运行进程,选择 idle 进程。
设计思想:这种层次结构保证了高优先级任务(如实时任务)能够优先获得 CPU,而普通任务在系统空闲时才能运行。同时,通过 next 指针的遍历机制,实现了调度类的“优先级继承”效果。
Linux 调度类层次结构 stop_sched_class(最高优先级) dl_sched_class(截止时间调度) rt_sched_class(实时调度) fair_sched_class(完全公平调度) idle_sched_class(空闲调度 - 最低优先级) 箭头方向表示优先级降低:高优先级调度类无任务时,通过 next 指针降级到下一个调度类

4.4 调度类与调度实体的协作

调度实体和调度类在运行时紧密协作,共同完成进程调度:

  • 调度类负责“算法”:定义如何选择下一个运行进程、如何管理运行队列、如何处理抢占等。
  • 调度实体负责“数据”:存储每个进程的调度状态(vruntime、权重、执行时间等),供调度类算法使用。
  • 运行队列(rq)作为桥梁:每个 CPU 有一个运行队列,包含多个子队列(cfs_rq、rt_rq、dl_rq),分别由对应的调度类管理。

4.4.1 典型调度流程

// 简化后的调度主路径
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    const struct sched_class *class;
    struct task_struct *p;

    // 1. 从最高优先级调度类开始尝试
    for_each_class(class) {
        p = class->pick_next_task(rq);
        if (p)
            return p;  // 找到可运行进程
    }

    // 2. 所有调度类都无任务,返回 idle 进程
    return rq->idle;
}
理解要点:for_each_class 宏遍历调度类链表,从 stop_sched_class 开始,依次调用 pick_next_task。每个调度类内部使用自己的数据结构(如 CFS 的红黑树、RT 的优先级数组)来选择最优进程。这种分层设计使得内核可以轻松添加新的调度策略,只需实现 sched_class 接口并插入到链表中即可。

4.5 本章小结

概念 核心作用 关键字段/方法
调度实体 可调度单元,存储调度状态 vruntime、load、run_node
调度类 调度算法实现,定义调度策略 pick_next_task、enqueue_task、task_tick
调度类层次 按优先级组织调度类,保证实时性 next 指针、for_each_class 遍历
运行队列 每个 CPU 的调度数据结构容器 cfs_rq、rt_rq、dl_rq