2. 调度实体与调度类:task_struct中的调度字段、sched_entity结构体、调度类接口定义

在Linux内核中,进程调度并非直接操作task_struct的全部字段,而是通过调度实体(Scheduling Entity)和调度类(Scheduling Class)两层抽象来实现。这种设计使得CFS(完全公平调度器)与其他调度策略(如实时调度)能够共存于同一系统,且互不干扰。本节将深入剖析这三个核心概念及其在源码中的具体实现。

2.1 task_struct中的调度相关字段

每个进程在内核中由task_struct表示,其中包含了调度器所需的所有元数据。以下是与调度直接相关的关键字段:

字段名 类型 作用描述
prio int 动态优先级,调度器实际使用的优先级值(数值越小优先级越高)
static_prio int 静态优先级,由nice值计算得出,仅在调整nice值时改变
normal_prio int 常规优先级,基于静态优先级和调度策略计算
rt_priority unsigned int 实时进程优先级(0-99),仅用于实时调度类
sched_class const struct sched_class * 指向当前进程所属调度类的指针,是调度策略多态的关键
se struct sched_entity CFS调度实体,包含虚拟运行时间、权重等核心调度数据
rt struct sched_rt_entity 实时调度实体,用于实时进程的调度管理
dl struct sched_dl_entity Deadline调度实体,用于EDF(最早截止时间优先)调度
state volatile long 进程状态(运行、可中断睡眠、不可中断睡眠等)
on_rq int 标记进程是否在就绪队列上
设计要点: sched_class指针是Linux调度器实现策略与机制分离的核心。调度器通过该指针调用不同调度类的函数,而不需要关心具体实现细节。这种面向对象的设计思想(C语言通过函数指针实现)使得添加新的调度策略变得非常容易。

2.2 sched_entity结构体:CFS的调度单元

sched_entity是CFS调度器操作的基本单位,它独立于task_struct存在,使得CFS可以调度单个进程,也可以调度进程组(组调度)。其核心字段如下:

// 位于 include/linux/sched.h
struct sched_entity {
    /* 负载权重,用于计算时间片分配比例 */
    struct load_weight      load;
    /* 红黑树节点,用于将调度实体插入CFS就绪队列的红黑树 */
    struct rb_node          run_node;
    /* 所属的CFS就绪队列 */
    struct cfs_rq           *cfs_rq;
    /* 所属的组就绪队列(仅当该实体代表一个任务组时有效) */
    struct cfs_rq           *my_q;

    /* 虚拟运行时间,CFS调度的核心度量 */
    u64                     vruntime;
    /* 上一次更新统计信息的时间戳 */
    u64                     prev_sum_exec_runtime;
    /* 总执行时间 */
    u64                     sum_exec_runtime;
    /* 进程已使用的虚拟时间片 */
    u64                     exec_start;

    /* 用于记录调度实体被插入就绪队列的次数 */
    unsigned long           nr_migrations;

    /* 统计信息:是否在就绪队列上、是否被延迟等 */
#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity     *parent;
    /* 组调度相关的链表节点 */
    struct list_head        group_node;
#endif
};
关键理解: vruntime(虚拟运行时间)是CFS调度算法的灵魂。CFS总是选择vruntime最小的调度实体来运行。每次时钟中断时,当前运行进程的vruntime会增加,增加量等于实际运行时间乘以权重系数。权重越大的进程,其vruntime增长越慢,从而获得更多的CPU时间。

下图展示了sched_entity与task_struct、cfs_rq之间的关系:

调度实体与进程、就绪队列的关系 task_struct • prio / static_prio • sched_class * • se (sched_entity) • rt (sched_rt_entity) • dl (sched_dl_entity) sched_entity • load (权重) • run_node (红黑树节点) • vruntime (核心度量) • cfs_rq * (所属队列) • my_q * (组队列) cfs_rq • rb_root (红黑树根) • min_vruntime • curr (当前运行实体) • nr_running • load (队列总权重) se 字段 cfs_rq 指针 结构关系说明: 1. 每个 task_struct 内嵌一个 sched_entity 实例(se 字段),用于CFS调度。 2. sched_entity 通过 run_node 字段挂入 cfs_rq 的红黑树。 3. cfs_rq 维护一棵红黑树,树中所有 sched_entity 按 vruntime 排序。 4. 组调度时,一个 sched_entity 可以对应一个任务组,其 my_q 指向子 cfs_rq。 5. 调度器通过 task_struct->sched_class 选择调度类,CFS类内部操作 sched_entity。

2.3 调度类接口定义

调度类(struct sched_class)是Linux内核调度框架中最重要的抽象接口。它定义了一组函数指针,每个调度策略(CFS、实时、Deadline、空闲)都需要实现这些接口。调度器核心代码通过统一的方式调用这些接口,而不需要知道具体调度策略的实现细节。

// 位于 kernel/sched/sched.h
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);
    /* 主动放弃CPU */
    void (*yield_task)   (struct rq *rq);
    /* 检查是否可抢占当前进程 */
    void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);

    /* 选择下一个要运行的进程 */
    struct task_struct *(*pick_next_task)(struct rq *rq);
    /* 将进程放回就绪队列(通常与pick_next_task配对) */
    void (*put_prev_task)(struct rq *rq, struct task_struct *p);

#ifdef CONFIG_SMP
    /* 在SMP系统中选择目标CPU */
    int  (*select_task_rq)(struct task_struct *p, int cpu, int sd_flag, int flags);
    /* 迁移任务到另一个CPU */
    void (*migrate_task_rq)(struct task_struct *p, int new_cpu);
    /* 任务唤醒时的CPU选择 */
    void (*task_woken)(struct rq *this_rq, struct task_struct *task);
#endif

    /* 设置调度参数 */
    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);

#ifdef CONFIG_FAIR_GROUP_SCHED
    /* 组调度相关:任务组变更时的处理 */
    void (*task_change_group)(struct task_struct *p, int type);
#endif
};
重要提示: 调度类接口的设计遵循开闭原则——对扩展开放,对修改封闭。开发者可以编写新的调度类(如实现一个自定义的能耗感知调度器),只需实现上述接口,然后将其注册到调度类链表中即可,无需修改调度核心代码。

2.4 调度类优先级链

Linux内核中的调度类按照优先级顺序链接在一起,形成一个单向链表。调度器在选择下一个进程时,按照优先级从高到低的顺序遍历各个调度类,直到找到可运行的进程。

调度类 优先级顺序 适用场景 核心算法
stop_sched_class 最高(0) 停止CPU的紧急任务 直接停止当前CPU
dl_sched_class 1 有严格截止时间的实时任务 EDF(最早截止时间优先)
rt_sched_class 2 POSIX实时进程(SCHED_FIFO/SCHED_RR) 固定优先级抢占
fair_sched_class 3 普通分时进程(SCHED_NORMAL/SCHED_BATCH) CFS(完全公平调度)
idle_sched_class 最低(4) 空闲进程(CPU无事可做时) 运行空闲线程

下图展示了调度类优先级链以及调度器选择进程的流程:

调度类优先级链与进程选择流程 stop_sched_class 优先级 0 dl_sched_class 优先级 1 rt_sched_class 优先级 2 fair_sched_class 优先级 3 idle_sched_class 优先级 4 pick_next_task 执行流程: 1. 从 stop_sched_class 开始,调用 pick_next_task 检查是否有可运行进程。 2. 若当前调度类有可运行进程,则直接返回该进程,不再检查后续调度类。 3. 若当前调度类无进程可运行,则通过 next 指针进入下一个调度类。 4. 依次检查 dl → rt → fair → idle,直到找到可运行进程。 5. idle_sched_class 保证始终有一个空闲线程可以运行(CPU不会空转)。 6. 这种设计确保实时进程优先于普通进程,普通进程优先于空闲进程。 7. 每个调度类内部使用自己的数据结构(如CFS的红黑树)来管理进程。

2.5 调度实体与调度类的协同工作

理解调度实体和调度类如何协同工作,是掌握CFS调度器的关键。以下是它们之间的协作流程:

  1. 进程创建时: sched_fork() 函数根据进程的调度策略设置 task_struct->sched_class 指针,并初始化对应的调度实体(如 se 字段)。
  2. 进程加入就绪队列: 调度核心调用 sched_class->enqueue_task(),CFS类将 sched_entity 插入红黑树。
  3. 进程选择时: pick_next_task() 按优先级遍历调度类链表,CFS类从红黑树中选出 vruntime 最小的 sched_entity。
  4. 时钟中断时: sched_class->task_tick() 更新当前进程的 vruntime,并检查是否需要抢占。
  5. 进程退出时: sched_class->task_dead() 清理调度实体占用的资源。
组调度扩展: 当启用 CONFIG_FAIR_GROUP_SCHED 时,一个 sched_entity 可以代表一个任务组(如Docker容器中的所有进程)。该实体的 my_q 指向一个子 cfs_rq,子队列中再包含该组内各个进程的 sched_entity。这种嵌套结构使得CFS能够实现层级化的CPU时间分配,是容器化环境中CPU资源隔离的基础。

通过这种分层设计,Linux内核实现了调度策略的高度模块化和可扩展性。开发者可以专注于实现特定调度类的接口,而调度核心框架则负责统一的进程管理和CPU分配逻辑。