2. 进程描述符与调度实体

进程调度是内核最核心的子系统之一。要理解 CFS 调度器与负载均衡,首先必须掌握内核如何描述一个进程、如何抽象调度单位,以及如何通过多态接口组织不同的调度策略。本章从 task_struct 的关键字段出发,逐步深入到调度实体 sched_entity 的结构,最后剖析调度类 sched_class 的接口设计。

2.1 task_struct:进程的完整画像

在 Linux 内核中,每个进程都由一个 task_struct 结构体描述。该结构体定义在 include/linux/sched.h 中,是内核中最大的结构体之一,包含数百个字段。以下是与调度和负载均衡直接相关的关键字段分组。

字段分组 关键字段 作用说明
进程标识 pid, tgid 进程 ID 和线程组 ID,用于唯一标识和线程组管理
调度状态 state, exit_state 进程运行状态(TASK_RUNNING、TASK_INTERRUPTIBLE 等)
调度参数 prio, static_prio, normal_prio, rt_priority 动态优先级、静态优先级、实时优先级
调度实体 se (struct sched_entity) CFS 调度实体,包含虚拟时间、权重等
实时调度实体 rt (struct sched_rt_entity) 实时调度实体,用于 RT 调度类
调度类 sched_class 指向当前使用的调度类(多态)
CPU 亲和性 cpus_allowed 允许进程运行的 CPU 位图
运行队列 on_rq 标记进程是否在运行队列中
统计信息 utime, stime, vruntime (通过 se) 用户态/内核态时间、虚拟运行时间

关键理解: task_struct 并不直接包含调度算法逻辑,而是通过 sched_class 指针和 se/rt 实体将调度行为委托给具体的调度类。这种设计体现了面向对象的多态思想。

2.2 sched_entity:CFS 的调度原子

sched_entity 是 CFS 调度器操作的基本单位。每个 task_struct 内嵌一个 se 字段,CFS 不直接操作进程,而是操作调度实体。这使得 CFS 可以统一调度普通进程和调度组(group scheduling)。

// include/linux/sched.h (简化)
struct sched_entity {
    struct load_weight      load;           /* 调度权重 */
    struct rb_node          run_node;       /* 红黑树节点 */
    struct list_head        group_node;     /* 组调度链表节点 */
    unsigned int            on_rq;          /* 是否在运行队列中 */

    u64                     vruntime;       /* 虚拟运行时间 */
    u64                     prev_sum_exec_runtime; /* 上次调度时的总执行时间 */

    u64                     nr_migrations;  /* 迁移次数 */

    struct sched_statistics statistics;     /* 调度统计 */
};

核心字段解析:

  • load (权重): 决定进程在 CPU 时间分配中的比例。普通进程的权重由 nice 值映射得到,映射表为 prio_to_weight。
  • vruntime (虚拟运行时间): CFS 的核心指标。进程每执行 1 个实际时间单位,vruntime 增加 实际时间 * (NICE_0_LOAD / 当前权重)。CFS 总是选择 vruntime 最小的进程运行。
  • run_node: 红黑树节点。所有可运行的 CFS 实体按 vruntime 组织在一棵红黑树中,最左节点即为下一个要运行的进程。

权重与 nice 的映射关系: nice 值每增加 1,CPU 时间配额减少约 10%。nice=0 的权重为 1024,nice=19 的权重为 15,nice=-20 的权重为 88761。这种非线性映射保证了低优先级进程仍能获得少量 CPU 时间。

sched_entity 与 CFS 红黑树结构 struct sched_entity load (权重) vruntime (虚拟时间) run_node (红黑树节点) on_rq (运行队列标记) statistics (统计信息) nr_migrations (迁移次数) CFS 红黑树 (按 vruntime 排序) vr=120 vr=80 vr=200 vr=45 vr=100 最左节点 → 下一个运行进程 vr = vruntime (越小越优先)

2.3 调度类 (sched_class):多态调度接口

Linux 内核通过调度类实现不同调度策略的插件化。每个调度类实现一组标准接口,内核通过 task_struct->sched_class 指针调用对应的方法。这种设计使得添加新的调度策略无需修改核心调度代码。

// 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);
    void (*yield_task)   (struct rq *rq);

    /* 进程选择 */
    struct task_struct *(*pick_next_task) (struct rq *rq);

    /* 时钟滴答处理 */
    void (*task_tick)    (struct rq *rq, struct task_struct *p, int queued);

    /* 进程迁移 */
    void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask);

    /* 负载均衡 */
    unsigned long (*balance) (struct rq *rq, struct task_struct *prev, struct rq_flags *rf);

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

核心接口说明:

接口方法 触发时机 典型实现 (CFS)
enqueue_task 进程变为可运行状态 将 sched_entity 插入红黑树,更新 vruntime
dequeue_task 进程阻塞或退出 从红黑树移除,更新统计信息
pick_next_task 调度点选择下一个进程 取红黑树最左节点,更新上下文
task_tick 周期性时钟中断 更新当前进程的 vruntime,检查是否需要抢占
balance 负载均衡触发 计算当前 CPU 负载,决定是否拉取任务
update_curr 每次时钟 tick 或调度点 更新当前进程的执行时间和 vruntime

调度类优先级顺序: 内核按 stop_sched_class → dl_sched_class → rt_sched_class → fair_sched_class → idle_sched_class 的顺序遍历调度类。高优先级调度类有任务时,低优先级类不会获得 CPU。

调度类接口与多态调用流程 task_struct pid, state, prio se (sched_entity) sched_class * 调度类链表 (sched_class) stop_sched_class dl_sched_class rt_sched_class fair_sched_class (CFS) idle_sched_class 统一接口方法 enqueue_task() dequeue_task() pick_next_task() task_tick() balance() update_curr() 调度核心通过 sched_class 指针调用接口,无需关心具体调度策略实现 — 面向对象多态设计

2.4 三者的协作关系

task_struct、sched_entity 和 sched_class 三者构成了 Linux 调度系统的核心骨架:

  1. task_struct 是进程的容器,持有调度所需的所有元数据。
  2. sched_entity 是 CFS 调度的原子单位,内嵌于 task_struct,包含权重、vruntime 等调度关键数据。
  3. sched_class 提供一组标准接口,task_struct 通过指针引用具体的调度类实现。

设计精髓: 这种分层解耦的设计使得内核可以同时支持多种调度策略。实时进程使用 rt_sched_class,普通进程使用 fair_sched_class,空闲进程使用 idle_sched_class。调度核心代码只需调用 sched_class 的接口,无需关心底层实现细节。