2. 内核调度器基础:CFS调度器原理、调度实体与运行队列、调度周期与时间片

在深入分析 Hung Task 故障之前,必须理解 Linux 内核调度器的核心——完全公平调度器(CFS)。CFS 是 Linux 2.6.23 引入的默认调度类,其设计哲学是“让每个进程尽可能公平地获得 CPU 时间”。本节将拆解 CFS 的三大支柱:调度实体(sched_entity)、红黑树运行队列(cfs_rq)以及调度周期与时间片计算。

2.1 CFS 调度器核心原理

CFS 摒弃了传统的时间片轮转与优先级队列,转而使用基于 虚拟运行时间(vruntime) 的模型。每个可运行的调度实体(SE)都维护一个 vruntime,CFS 总是选择 vruntime 最小的实体来运行。这保证了所有实体在宏观上获得等比例的 CPU 时间。

关键概念: vruntime 是经过优先级加权的实际运行时间。优先级高的进程(nice 值小)的 vruntime 增长更慢,因此更容易被选中,从而获得更多 CPU 时间。

CFS 使用红黑树(Red-Black Tree)作为就绪队列的数据结构。红黑树是一棵自平衡二叉搜索树,其左子节点的 vruntime 总是小于父节点,右子节点的 vruntime 总是大于父节点。因此,树的最左叶子节点就是当前 vruntime 最小的实体,即下一个要运行的进程。

故障排查提示: 当系统出现 Hung Task 时,检查 /proc/sched_debug 或使用 ftrace 跟踪 cfs_rq 的红黑树状态,可以判断是否有进程因 vruntime 异常(如溢出或停滞)而长期无法被调度。
CFS 调度器核心架构 CFS 调度器核心架构 红黑树根节点 左子节点 右子节点 最左叶子节点 (vruntime 最小) 内部节点 内部节点 右叶子节点 调度决策流程: 1. 每次时钟中断或进程状态变化时,CFS 检查当前进程的 vruntime。 2. 如果当前进程的 vruntime 超过了红黑树最左叶子节点的 vruntime,则触发抢占。 3. 调度器选择最左叶子节点对应的调度实体投入运行。 4. 被换出的进程重新插入红黑树,位置由其新的 vruntime 决定。

2.2 调度实体(sched_entity)与运行队列(cfs_rq)

调度实体是 CFS 调度的基本单位。它可以是单个任务(task),也可以是任务组(task group)。每个 CPU 都有一个独立的 cfs_rq 结构体,用于管理该 CPU 上所有可运行的 CFS 调度实体。

2.2.1 调度实体结构

调度实体 struct sched_entity 是嵌入在 struct task_struct 中的关键字段。它记录了进程的调度状态、权重、vruntime 以及指向红黑树节点的指针。

// include/linux/sched.h (简化结构)
struct sched_entity {
    struct load_weight      load;           // 调度权重,由 nice 值决定
    struct rb_node          run_node;       // 红黑树节点
    struct list_head        group_node;     // 用于任务组链表
    unsigned int            on_rq;          // 是否在就绪队列上
    u64                     vruntime;       // 虚拟运行时间
    u64                     prev_sum_exec_runtime; // 上次切换时的总执行时间
    u64                     sum_exec_runtime;      // 总执行时间
    struct sched_entity     *parent;        // 指向父调度实体(用于任务组)
};
Hung Task 关联点: 如果 on_rq 为 1 但进程长时间未被调度,检查其 vruntime 是否异常巨大(导致被红黑树“遗忘”在右侧深处),或者 sum_exec_runtime 是否停滞增长(表明进程可能被错误地挂起)。

2.2.2 运行队列结构

每个 CPU 的 struct cfs_rq 管理着该 CPU 上所有 CFS 调度实体。它维护了红黑树的根节点、最小 vruntime 值以及运行队列的统计信息。

字段 类型 说明
load struct load_weight 该运行队列上所有实体的总权重
nr_running unsigned int 当前可运行的实体数量
min_vruntime u64 队列中最小的 vruntime,用于新进程的 vruntime 初始化
tasks_timeline struct rb_root_cached 红黑树根节点(包含缓存的最左节点)
curr struct sched_entity * 当前正在运行的调度实体
next struct sched_entity * 下一个将要运行的调度实体(抢占优化)
调度实体与运行队列关系 调度实体与运行队列关系 CPU 0 cfs_rq 红黑树根节点 SE1 (PID 100) vruntime: 1000 SE2 (PID 200) vruntime: 1200 SE3 (PID 300) vruntime: 1500 curr 关键关系说明: • 每个 CPU 拥有独立的 cfs_rq。 • cfs_rq 通过红黑树组织所有可运行的调度实体。 • curr 指针指向当前正在 CPU 上执行的实体。 • 红黑树中的实体按 vruntime 排序。 • 新创建的实体被插入红黑树,其 vruntime 初始化为 min_vruntime。 • 当进程被唤醒时,重新插入红黑树。 • 当进程阻塞或退出时,从红黑树移除。 • Hung Task 可能表现为实体在红黑树中但从未被选中。

2.3 调度周期与时间片

调度周期(sched_latency_ns)是 CFS 保证每个可运行进程至少被调度一次的时间窗口。时间片(sched_slice)则是在该周期内,每个进程获得的 CPU 时间份额。

2.3.1 调度周期

调度周期由内核参数 kernel.sched_latency_ns 控制,默认值为 6,000,000 ns(6 毫秒)。当可运行进程数量较少时,调度周期固定为 sched_latency_ns。当进程数量增多时,为了保持响应性,调度周期会线性增长,但受限于 sched_min_granularity_ns(最小粒度,默认 0.75 ms)。

// kernel/sched/fair.c (简化逻辑)
static u64 __sched_period(unsigned long nr_running) {
    if (nr_running > sched_nr_latency) {
        // 进程数超过阈值,周期线性增长
        return nr_running * sysctl_sched_min_granularity;
    } else {
        return sysctl_sched_latency;
    }
}

2.3.2 时间片计算

每个进程的时间片由其权重(weight)与运行队列总权重的比值决定。权重由 nice 值映射而来,nice 值越低(优先级越高),权重越大,获得的时间片越长。

Nice 值 权重(weight) 相对时间片比例
-20 88761 约 10.0x
-10 29154 约 3.3x
0 1024 1.0x(基准)
10 304 约 0.3x
19 15 约 0.015x

时间片计算公式如下:

// 时间片 = 调度周期 * (进程权重 / 运行队列总权重)
slice = period * se->load.weight / cfs_rq->load.weight;
故障排查技巧: 当系统出现大量 CPU 密集型进程时,调度周期会变长,每个进程的时间片会缩短。如果某个进程的时间片过小(接近 sched_min_granularity_ns),频繁的上下文切换可能导致调度延迟,进而触发 Hung Task 检测。可以通过调整 /proc/sys/kernel/sched_latency_ns 和 /proc/sys/kernel/sched_min_granularity_ns 来缓解。
调度周期与时间片分配流程 调度周期与时间片分配流程 调度周期 (sched_latency_ns = 6ms) 进程 A 时间片 权重: 1024 (nice 0) 进程 B 时间片 权重: 2048 (nice -5) 进程 C 时间片 权重: 512 (nice 5) 进程 D 时间片 权重: 1024 (nice 0) 时间片分配示例(总权重 = 1024+2048+512+1024 = 4608): • 进程 A 时间片 = 6ms * (1024 / 4608) ≈ 1.33ms • 进程 B 时间片 = 6ms * (2048 / 4608) ≈ 2.67ms(优先级最高,获得最长时间片) • 进程 C 时间片 = 6ms * (512 / 4608) ≈ 0.67ms(优先级最低,获得最短时间片)

2.4 调度器故障与 Hung Task 的关联

理解 CFS 基础后,可以归纳出与 Hung Task 相关的几个关键故障模式:

  • vruntime 异常: 由于内核 bug 或优先级继承问题,导致某个进程的 vruntime 被设置为极大值,使其在红黑树中“沉底”,几乎永远不会被调度。
  • 运行队列锁竞争: 当多个 CPU 同时操作同一个 cfs_rq 时(例如在负载均衡场景下),如果锁持有时间过长,可能导致进程无法被加入或移出运行队列。
  • 调度周期过长: 当可运行进程数量极大时(例如数千个线程),调度周期可能膨胀到秒级,导致单个进程等待时间超过 120 秒的 Hung Task 阈值。
  • 时间片过小: 频繁的上下文切换导致调度器开销过大,实际用于进程执行的时间极少,表现为进程“假死”。
排查切入点: 在 Hung Task 日志中,如果看到 blocked for more than 120 seconds 且调用栈指向 schedule() 或 __schedule(),应优先检查该进程的 vruntime 和所在 cfs_rq 的状态。使用 cat /proc/<PID>/sched 可以查看进程的调度统计信息。