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 时间。
CFS 使用红黑树(Red-Black Tree)作为就绪队列的数据结构。红黑树是一棵自平衡二叉搜索树,其左子节点的 vruntime 总是小于父节点,右子节点的 vruntime 总是大于父节点。因此,树的最左叶子节点就是当前 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; // 指向父调度实体(用于任务组)
};
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 * |
下一个将要运行的调度实体(抢占优化) |
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;
sched_min_granularity_ns),频繁的上下文切换可能导致调度延迟,进而触发 Hung Task 检测。可以通过调整 /proc/sys/kernel/sched_latency_ns 和 /proc/sys/kernel/sched_min_granularity_ns 来缓解。
2.4 调度器故障与 Hung Task 的关联
理解 CFS 基础后,可以归纳出与 Hung Task 相关的几个关键故障模式:
- vruntime 异常: 由于内核 bug 或优先级继承问题,导致某个进程的 vruntime 被设置为极大值,使其在红黑树中“沉底”,几乎永远不会被调度。
- 运行队列锁竞争: 当多个 CPU 同时操作同一个
cfs_rq时(例如在负载均衡场景下),如果锁持有时间过长,可能导致进程无法被加入或移出运行队列。 - 调度周期过长: 当可运行进程数量极大时(例如数千个线程),调度周期可能膨胀到秒级,导致单个进程等待时间超过 120 秒的 Hung Task 阈值。
- 时间片过小: 频繁的上下文切换导致调度器开销过大,实际用于进程执行的时间极少,表现为进程“假死”。
blocked for more than 120 seconds 且调用栈指向 schedule() 或 __schedule(),应优先检查该进程的 vruntime 和所在 cfs_rq 的状态。使用 cat /proc/<PID>/sched 可以查看进程的调度统计信息。