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) 完全公平,良好的交互性,可扩展性强
调优要点: 理解调度器演进的关键在于认识到,每一次变革都是为了解决特定场景下的性能瓶颈。O(1) 解决了大规模进程下的调度开销问题,而 CFS 则进一步优化了公平性和交互式应用的响应体验。

2.2 O(1) 调度器:里程碑式的突破

O(1) 调度器是 Linux 2.6 内核早期版本的标志性改进,其名称来源于调度算法的时间复杂度为常数级 O(1),即无论系统中运行多少个进程,调度决策的时间都是固定的。

2.2.1 核心数据结构:双优先级数组

O(1) 调度器的核心是使用两个优先级数组:active(活跃)和 expired(过期)。每个数组包含 140 个优先级队列(0-139,0 为最高优先级)。

O(1) 调度器双优先级数组结构 Active 数组 (活跃队列) 140-bit 活跃位图 (查找最高优先级) Prio 0 (实时) Prio 1 (实时) Prio 100-139 (普通/分时) Expired 数组 (过期队列) 140-bit 过期位图 Prio 0 (实时) Prio 1 (实时) Prio 100-139 (普通/分时) 当 active 为空时,交换指针 (swap)

2.2.2 O(1) 调度流程

  1. 选择进程: 通过位图操作在 active 数组中快速找到优先级最高的非空队列,取出队首进程。
  2. 时间片耗尽: 当前进程时间片用完后,计算其新的优先级,并将其放入 expired 数组的对应队列。
  3. 数组切换: 当 active 数组为空时,交换 active 和 expired 指针,使 expired 成为新的 active。
局限性: O(1) 调度器虽然解决了性能问题,但其优先级计算机制对交互式进程的识别不够准确,导致桌面响应有时不够流畅。此外,其“时间片”机制在负载变化时不够灵活。

2.3 CFS 调度器:完全公平的典范

从 Linux 2.6.23 开始,CFS (Completely Fair Scheduler) 取代 O(1) 成为默认调度器。CFS 摒弃了传统的“时间片 + 优先级”模型,转而采用基于 虚拟运行时间 (vruntime) 的模型。

2.3.1 核心原理:红黑树与 vruntime

CFS 维护一棵以 vruntime 为键值的红黑树。vruntime 是进程实际运行时间经过优先级加权后的值。调度器总是选择 vruntime 最小的进程(即红黑树最左节点)来运行,从而保证所有进程在宏观上获得公平的 CPU 时间。

CFS 调度器红黑树结构 vruntime: 100ms vruntime: 50ms vruntime: 200ms vruntime: 30ms vruntime: 80ms 调度决策: 每次调度时,选择红黑树最左节点(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 调度类层次结构

内核维护一个调度类优先级链表,调度时按优先级从高到低遍历各个调度类,选择其中优先级最高的进程运行。

调度类优先级链与协同机制 Stop 调度类 (最高优先级,用于 CPU 停止) RT 调度类 (SCHED_FIFO / SCHED_RR,优先级 0-99) CFS 调度类 (SCHED_NORMAL / SCHED_BATCH,优先级 100-139) Idle 调度类 (最低优先级,空闲任务)

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 获得少量时间
调优陷阱: 如果实时进程设计不当(例如在 SCHED_FIFO 中执行死循环),会导致 CFS 进程完全无法运行,系统可能因关键服务(如 sshd)被饿死而变得不可管理。务必为实时任务添加合理的阻塞点或使用 SCHED_RR。

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) 通过严格的优先级层次实现协同,确保实时任务获得确定性响应,同时通过带宽限制机制防止实时任务饿死系统。理解这些机制是进行实时内核性能调优的理论基石。