2. Linux 内核抢占模型:调度器演进与协同机制
本章深入剖析 Linux 内核抢占模型的核心——调度器的发展历程与协同工作原理。从早期的 O(1) 调度器到当前主流的 CFS 调度器,再到实时调度类 (RT) 与分时调度类 (CFS) 的协同机制,理解这些演进是进行实时性能调优的基础。
2.1 Linux 内核调度器发展史
Linux 内核调度器经历了从简单到复杂、从粗糙到精细的演进过程,每一次变革都旨在更好地平衡吞吐量、响应时间和公平性。
| 阶段 | 内核版本 | 调度器名称 | 核心算法 | 主要特点 |
|---|---|---|---|---|
| 1 | 2.4 及之前 | 原始调度器 | 循环链表 + 优先级 | O(n) 复杂度,进程数多时性能急剧下降 |
| 2 | 2.6.0 - 2.6.22 | O(1) 调度器 | 双优先级数组 (active/expired) | O(1) 复杂度,引入 SMP 负载均衡 |
| 3 | 2.6.23 至今 | CFS 调度器 | 红黑树 + 虚拟运行时间 (vruntime) | 完全公平,良好的交互性,可扩展性强 |
2.2 O(1) 调度器:里程碑式的突破
O(1) 调度器是 Linux 2.6 内核早期版本的标志性改进,其名称来源于调度算法的时间复杂度为常数级 O(1),即无论系统中运行多少个进程,调度决策的时间都是固定的。
2.2.1 核心数据结构:双优先级数组
O(1) 调度器的核心是使用两个优先级数组:active(活跃)和 expired(过期)。每个数组包含 140 个优先级队列(0-139,0 为最高优先级)。
2.2.2 O(1) 调度流程
- 选择进程: 通过位图操作在 active 数组中快速找到优先级最高的非空队列,取出队首进程。
- 时间片耗尽: 当前进程时间片用完后,计算其新的优先级,并将其放入 expired 数组的对应队列。
- 数组切换: 当 active 数组为空时,交换 active 和 expired 指针,使 expired 成为新的 active。
2.3 CFS 调度器:完全公平的典范
从 Linux 2.6.23 开始,CFS (Completely Fair Scheduler) 取代 O(1) 成为默认调度器。CFS 摒弃了传统的“时间片 + 优先级”模型,转而采用基于 虚拟运行时间 (vruntime) 的模型。
2.3.1 核心原理:红黑树与 vruntime
CFS 维护一棵以 vruntime 为键值的红黑树。vruntime 是进程实际运行时间经过优先级加权后的值。调度器总是选择 vruntime 最小的进程(即红黑树最左节点)来运行,从而保证所有进程在宏观上获得公平的 CPU 时间。
2.3.2 CFS 关键参数与调优
| 参数 | 路径 | 默认值 | 调优说明 |
|---|---|---|---|
sched_latency_ns |
/proc/sys/kernel/ |
6000000 (6ms) | 目标调度延迟。减小可提升交互性,但会增加上下文切换开销。 |
sched_min_granularity_ns |
/proc/sys/kernel/ |
750000 (0.75ms) | 进程最小运行时间。防止频繁切换导致的开销。 |
sched_wakeup_granularity_ns |
/proc/sys/kernel/ |
1000000 (1ms) | 唤醒抢占粒度。减小可让唤醒的进程更快抢占 CPU。 |
sched_latency_ns 和 sched_wakeup_granularity_ns,以降低调度延迟。但需注意,过度减小会导致 CPU 时间浪费在上下文切换上。
2.4 实时调度类 (RT) 与分时调度类 (CFS) 的协同
Linux 内核使用模块化调度类架构,允许不同的调度策略共存。实时调度类 (RT) 和分时调度类 (CFS) 是其中最重要的两个。
2.4.1 调度类层次结构
内核维护一个调度类优先级链表,调度时按优先级从高到低遍历各个调度类,选择其中优先级最高的进程运行。
2.4.2 协同规则与抢占行为
- 优先级绝对性: 只要 RT 调度类中有可运行的进程,CFS 调度类中的进程就永远不会获得 CPU。这是实时系统的基本保证。
- RT 内部调度:
- SCHED_FIFO: 先进先出,除非被更高优先级的 RT 进程抢占或主动让出 CPU,否则会一直运行。
- SCHED_RR: 时间片轮转,相同优先级的 RR 进程按时间片轮流运行。
- CFS 运行条件: 只有当所有 RT 进程都处于阻塞或停止状态时,CFS 调度类才会被选中。
2.4.3 实时进程与普通进程的共存策略
| 场景 | RT 进程行为 | CFS 进程行为 | 系统影响 |
|---|---|---|---|
| RT 进程持续运行 | 独占 CPU | 完全饥饿 | 系统可能无响应 (watchdog 可能触发) |
| RT 进程周期性阻塞 | 按周期运行,阻塞时让出 CPU | 在 RT 阻塞期间获得 CPU | 理想状态,实时与分时任务共存 |
| RT 进程使用 SCHED_RR | 时间片耗尽后让出 CPU | 在 RT 时间片间隙运行 | 保证 RT 公平性,CFS 获得少量时间 |
2.4.4 内核配置与运行时调优
内核编译选项和运行时参数共同决定了 RT 与 CFS 的协同行为。
# 内核编译选项 (Kernel Configuration)
# 启用实时调度类支持
CONFIG_PREEMPT_RT_FULL=y # 实时内核补丁 (PREEMPT_RT)
CONFIG_PREEMPT=y # 内核抢占支持
# 运行时参数调整
# 限制实时进程可以使用的 CPU 时间比例 (默认 95%)
echo 950000 > /proc/sys/kernel/sched_rt_runtime_us
# 设置实时进程的带宽周期 (默认 1000000 us = 1s)
echo 1000000 > /proc/sys/kernel/sched_rt_period_us
# 查看当前调度策略和优先级
chrt -p $$
# 输出示例: pid 1234's current scheduling policy: SCHED_OTHER
# pid 1234's current scheduling priority: 0
sched_rt_runtime_us 和 sched_rt_period_us,可以限制实时进程在每 1 秒内最多运行 950ms,从而为 CFS 进程保留 5% 的 CPU 时间,防止系统完全被实时任务锁死。
2.5 本章小结
Linux 内核调度器从 O(1) 演进到 CFS,体现了对公平性和交互性的极致追求。实时调度类 (RT) 与分时调度类 (CFS) 通过严格的优先级层次实现协同,确保实时任务获得确定性响应,同时通过带宽限制机制防止实时任务饿死系统。理解这些机制是进行实时内核性能调优的理论基石。