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 时间。
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。
2.4 三者的协作关系
task_struct、sched_entity 和 sched_class 三者构成了 Linux 调度系统的核心骨架:
- task_struct 是进程的容器,持有调度所需的所有元数据。
- sched_entity 是 CFS 调度的原子单位,内嵌于 task_struct,包含权重、vruntime 等调度关键数据。
- sched_class 提供一组标准接口,task_struct 通过指针引用具体的调度类实现。
设计精髓: 这种分层解耦的设计使得内核可以同时支持多种调度策略。实时进程使用 rt_sched_class,普通进程使用 fair_sched_class,空闲进程使用 idle_sched_class。调度核心代码只需调用 sched_class 的接口,无需关心底层实现细节。