Linux 调度子系统技术文档系列 · 第 9 篇
2019 年,某工业自动化公司部署了一套基于 Linux 的实时数据采集系统。为了降低传感器数据处理的延迟,工程师将所有采集进程设置为 SCHED_FIFO 最高优先级。上线第一天,系统表现完美——数据延迟从 5 毫秒降低到 50 微秒。第三天凌晨,整个产线突然卡死。监控画面凝固,报警系统静音,产线上的机械臂停在半空中。运维人员赶到现场后发现:一个 SCHED_FIFO 的日志线程进入了无限循环,由于它的优先级高于所有普通进程(包括 sshd、systemd-journald),系统变成了"砖头"——除了硬复位,没有任何恢复手段。
这不是虚构的故事,而是 RT throttling 机制诞生的真实诱因之一。Linux 的实时调度是一把双刃剑:它能给关键任务确定性的响应时间,也能在配置不当时让整个系统陷入瘫痪。

实时调度的核心诉求不是"快",而是"确定"。普通进程追求公平——每个进程最终都能分到 CPU;实时进程追求承诺——我必须在 2 毫秒内响应,这个承诺必须兑现。
Linux 内核提供了三种实时调度策略,每种策略对应不同的使用场景:
这三种策略共享一个关键特征:它们的优先级(1-99)始终高于普通进程的 nice 值(-20 到 19)。当 RT 进程就绪时,它会强制抢占当前运行的普通进程,这种抢占没有任何延迟。

理解实时调度的第一步,是看清它在内核中的数据组织方式。RT 进程和 Deadline 进程分别通过 sched_rt_entity 和 sched_dl_entity 两个结构体嵌入到 task_struct 中,每个进程只属于其中一个调度类。

run_list 是 RT 调度的关键——每个优先级(1-99)对应一个独立的 FIFO 链表。调度器从最高优先级开始扫描,找到第一个非空链表,取出队首任务执行。这种设计保证了 O(1) 的调度决策复杂度。
如果每次调度都要遍历 99 个优先级链表,效率会非常低。Linux 使用了一个巧妙的优化:优先级位图(priority bitmap)。

位图的每一位对应一个优先级。当某个优先级的队列从空变为非空时,对应位被置 1;当队列从非空变为空时,对应位被清零。调度器只需要用 find_first_bit() 指令(在 x86 上是单条 BSF 汇编指令)就能瞬间找到最高就绪优先级。这就像一本带索引的字典,你不需要从头翻起,直接跳到有内容的那一页。
Deadline 调度器的数据结构更加复杂,因为它需要跟踪任务的执行时间、周期和截止时间。

三参数模型是 Deadline 调度的核心设计。WCET(最坏执行时间)、周期和相对截止时间,这三个参数完整描述了一个周期性任务的时序特征。调度器根据这些参数计算带宽需求 dl_runtime / dl_period,并通过准入测试(Admission Test)判断系统是否有足够的剩余带宽接纳新任务。如果所有任务的带宽总和超过 CPU 容量,新任务会被拒绝。
回到文章开头的生产事故。SCHED_FIFO 进程进入死循环后,由于它没有主动让出 CPU 的时机(FIFO 策略没有时间片),也没有更高优先级的进程来抢占它,CPU 被 100% 占用。普通进程——包括负责网络通信的 sshd、负责日志的 journald、甚至负责终端的 getty——全部被饿死。
这是 RT 调度器的固有风险。为了解决这个问题,Linux 在 2.6.38 内核引入了 RT 带宽控制(RT Bandwidth Control)机制。
内核定义了两个关键参数:

这两个参数的含义是:在任意 1 秒的滑动窗口内,RT 进程最多只能运行 0.95 秒,剩余 0.05 秒强制留给普通进程。95% 这个数字并非随意选择——它基于一个经验法则:如果 RT 进程需要超过 95% 的 CPU,说明系统的实时负载设计本身就存在问题,应该重新评估架构,而不是用实时优先级来掩盖设计缺陷。
RT throttle 的检查嵌入在 RT 进程的运行时更新路径中,每次时钟中断都会执行:

当 rt_throttled 被置 1 后,RT 进程会被强制从运行队列中移除,CPU 会切换到普通进程。限流状态会持续到下一个周期(默认 1 秒)开始时自动解除。
这个机制带来了一个有趣的现象:如果你在音频处理中使用了 SCHED_FIFO,却发现音频偶尔卡顿,很可能不是你的代码有问题,而是 RT throttling 在起作用。一个经典的排查命令是:
# 查看当前 RT 带宽配置cat/proc/sys/kernel/sched_rt_period_us #1000000
cat/proc/sys/kernel/sched_rt_runtime_us # 950000
# 临时关闭限流(不推荐用于生产环境)echo-1>/proc/sys/kernel/sched_rt_runtime_us
chrt 命令是用户空间设置实时优先级的标准工具,它通过 sched_setscheduler() 系统调用与内核交互:

Deadline 调度器比 RT 调度更"聪明"——它不会让系统超载。在任务创建时,内核会执行准入测试(Admission Test),确保新任务不会导致已有任务错过截止时间。

准入测试是 Deadline 调度器的安全阀。它保证了一个数学定理:如果所有任务的带宽总和不超过 CPU 容量,且每个任务的 deadline >= runtime,那么所有任务都不会错过截止时间。这是 EDF 算法的最优性保证。
RT 调度基于静态优先级,它有一个根本性缺陷:优先级继承(Priority Inheritance)问题。假设系统中有两个 RT 任务:任务 A 优先级 90,每 100ms 执行 5ms;任务 B 优先级 80,每 50ms 执行 3ms。由于 A 的优先级更高,它总是抢占 B。但 A 的截止时间可能比 B 更宽松——A 可能每 10 秒才需要响应一次,而 B 每 50ms 就要响应一次。静态优先级无法表达"紧迫程度",它只能表达"重要程度"。
Deadline 调度器用 EDF(Earliest Deadline First)算法解决了这个问题。每个任务的优先级不是预先设定的,而是动态计算的——截止时间最早的任务获得最高优先级。这种策略在数学上被证明是最优的:如果一个任务集合可以被任何算法调度成功,EDF 一定也能调度成功。
引入 Deadline 调度器的第二个动机是带宽隔离。在 RT 调度中,一个失控的高优先级任务会饿死所有低优先级任务。而在 Deadline 调度中,每个任务的运行时间被严格限制在 dl_runtime 内。即使一个任务耗尽了它的运行时,它只会自己被限流(dl_throttled),不会影响其他 Deadline 任务的执行。这就像给每个任务分配了一个独立的"时间预算账户",花完了就等下个周期,不拖欠别人的额度。
实时调度是 Linux 对时间敏感任务的基础设施。RT 调度类(SCHED_FIFO/SCHED_RR)用静态优先级和抢占机制保证确定性响应,但需要 RT throttling 来防止系统被饿死。Deadline 调度器(SCHED_DEADLINE)用三参数模型和 EDF 算法提供更精确的时间保证,并通过准入测试确保系统不会超载。理解这些机制的 trade-off——确定性与灵活性、优先级与截止时间、抢占与限流——是在生产环境中正确使用实时调度的前提。
一个常见误区:认为设置实时优先级就能让程序"变快"。实时调度保证的是"按时",而不是"更快"。如果你的音频程序延迟高,首先要检查的是程序本身的逻辑,而不是盲目提升优先级。

思考题:
本系列文章基于 Linux 6.19.13 内核源码采用 CC BY-NC-SA 4.0 协议,转载请注明出处