把关键线程设置为 SCHED_FIFO,再把优先级调到 90 以上,看起来已经拿到了 CPU 的优先通行证。但在不少系统里,实际测到的调度延迟仍然有几百微秒,甚至偶尔跳到毫秒级。于是你会忍不住怀疑:调度器是不是没按优先级工作?

这种怀疑通常只对了一半。调度器确实尊重优先级,但它不是唯一决定 CPU 去向的部分。中断、软中断、内核临界区、锁的持有者,甚至调度器自己设的 RT 带宽限制,都可能临时让这张优先通行证失效。这篇文章就从几个常见延迟来源聊起,看看为什么 Linux RT 调度下高优先级线程仍然会被延迟,以及用什么方法把它定位出来。
先搞清楚:高优先级线程到底被什么挡在 CPU 外面
一个 SCHED_FIFO 线程能做到的事情很明确:只要 runqueue 里有比它优先级低的 CFS 任务,它可以选择抢占;只要 CPU 空闲,它一出现就会被运行。但这不等于它可以随时打断正在执行的内核代码。硬件中断、软中断、自旋锁保护的临界区,以及一部分原子操作路径,仍然会让 CPU 先忙完手头的事,再考虑你的线程。这里有一个常见的误区:把 SCHED_FIFO 看成“线程可以打断一切”。实际上它抢不过中断处理函数和禁用抢占的内核临界区。另一个误区是把所有尖峰都归因于调度器,而忽略锁和 RT 带宽控制。
真正麻烦的地方在于,这些障碍大多是瞬时的,不会出现在 top 或者 load average 里。它们只会在尾延迟分布上露出马脚。所以排查 RT 调度延迟时,第一步不是去怀疑调度器,而是拆分:这个线程从“就绪”到“真正在 CPU 上执行”,中间到底被哪一段挡了路。
最常见的嫌疑:中断和软中断
在非线程化中断的内核里,硬件中断处理函数的优先级天然高于所有线程,包括 SCHED_FIFO 99。如果网卡、NVMe、GPU 的中断集中在某个 CPU 上,而这个 CPU 恰好又是你的实时线程所在核,那么一次中断突发就可能造成几十微秒甚至更长的调度延迟。软中断通常在一个硬中断返回后,赶在返回用户态之前执行,这等于给 RT 线程又插入了一段看不见的高优先级片段;如果系统把软中断推给 ksoftirqd,影响会小一些,但实时性也更依赖内核线程的优先级设置。
一个典型的场景是:音频应用把音频线程提升到高优先级,却发现即使系统很空闲,延迟曲线里仍然出现周期性尖峰。打开 /proc/interrupts 一看,网卡中断和音频线程被调度到了同一个 CPU。中断频率和程序周期的频率一旦叠加,就会出现有规律的尾延迟。
先把中断分布看清楚:
cat /proc/interrupts
# 找到高频中断所在 CPU,再调整中断亲和性
echo 0x01 > /proc/irq/130/smp_affinity
0x01 表示只允许 CPU0 处理该中断。生产环境要更谨慎,通常建议把业务中断全部挪到非实时核,而为实时线程预留一个“干净”的 CPU。如果中断必须留在同核,可以尝试 threadirqs 启动参数,让中断主体变成 RT 内核线程,再配合优先级与你的应用线程排队。
一个容易忽略的“官方限速”:RT 带宽控制
Linux 还有一个反直觉的默认行为:为了保护 CFS 普通任务不被饿死,RT 调度类默认不是无限使用 CPU。内核通过 /proc/sys/kernel/sched_rt_runtime_us 和 sched_rt_period_us 控制运行预算,默认每 1 秒内 RT 任务最多运行 950000 微秒,也就是 95% CPU。超过之后,RT 运行队列会被节流,直到下个周期开始。
如果你有一个忙循环的 RT 线程,恰好占满整个 CPU,你会看到规律性的大跳变。那并不是被某个中断打断,而是调度器主动把 RT 任务“暂停”了。机器人控制这类场景很容易踩到:控制线程只做一些实时计算,如果它把时间片耗满,就会周期性地出现一次远高于平均值的抖动。
对于已经隔离出来专门跑实时任务的 CPU,通常可以关闭这个限制:
echo -1 > /proc/sys/kernel/sched_rt_runtime_us
但这要在确认实时任务不会失控的前提下操作,否则普通任务可能被完全饿死。
优先级反转:低优先级任务也能拖住实时线程
假设实时线程正在等待一把 mutex,而持有这把锁的是一个 CFS 普通线程。普通线程虽然优先级低,但只有它才能释放锁,实时线程再着急也只能等。如果那个低优先级线程偏偏被抢占、被迁移,或者也卡在别的事件上,实时线程的等待时间就会远远超过临界区本身。这就是典型的优先级反转。
普通 pthread mutex 不会自动解决这个问题。比较直接的做法是使用带优先级继承的 PI mutex:
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&lock, &attr);
但也要注意边界:用户态 PI 只能作用于用户态锁。如果实时线程在内核路径上等待某个低优先级 worker 完成的异步操作,优先级继承并不一定覆盖。排查时不能只查应用代码,还要看阻塞点是不是在驱动或内核服务里。
用 cyclictest 和 ftrace 把延迟“测”出来
没有量化,讨论优先级和被抢占都只是猜测。cyclictest 是测量调度延迟最常用的工具,它会创建一个高优先级线程,等待定时器,并统计实际唤醒时间与预期时间的差:
sudo cyclictest -t 1 -p 95 -n -m -i 1000 -H 1000
-i 指定 1ms 测试周期,-H 输出 1000us 以内的直方图。跑一段时间后,关注 max 值和分布尾部:正常 clean 场景下,多数采样在数十 us 以内,max 却可能突然跳到几百 us,这就是需要追的信号。
定位到延迟瞬间后,用 ftrace 抓取调度切换和中断事件,看延迟窗口附近谁在占 CPU:
sudo trace-cmd record -e sched_switch -e sched_wakeup -e irq_handler_entry -e irq_handler_exit -e softirq_entry -e softirq_exit ./your_rt_app
报告里几乎总能找到两类模式:如果 sched_wakeup 之后过了很久才有 sched_switch,问题主要在唤醒链路或 runqueue 选择;如果 sched_switch 之前插入了大量 irq/softirq,问题就在中断抢占。注意 ftrace 本身也会改变时序,最好只在小窗口内抓,抓完立刻关掉,再结合多次 cyclictest 结果看是否稳定复现。
几种调整手段怎么选
搞清楚了延迟源,接下来就不是简单“把优先级调高”了,而是根据系统约束决定手段。下面这张表是一个粗略的选型参考:
| 手段 | 解决的问题 | 需要接受的代价 | 适用系统 |
|---|---|---|---|
| CPU 隔离与 IRQ 亲和性 | 中断/负载均衡干扰实时核 | 会牺牲部分业务 CPU 资源 | 多核系统中已经有专用实时核 |
| 中断线程化 threadirqs | 让中断与 RT 线程同层排队 | 中断原始响应变慢,线程优先级要手动调 | 中断频率高但每个中断处理不极短 |
| PI 优先级继承 | 用户态锁持有者优先级过小 | 只能覆盖用户态 futex 路径 | 多线程共享资源的应用 |
| PREEMPT_RT 内核 | 减少不可抢占的内核临界区 | 内核维护成本高,驱动需要重新验证 | 对尾延迟要求严格、硬件可控的系统 |
这几种手段不是互斥的。多数严格实时系统会同时做 CPU 隔离、中断亲和性调整和 PI 互斥锁,然后再根据尾延迟实测决定是否上 PREEMPT_RT。
落地排查清单
如果你现在正被高优先级线程的随机延迟困扰,建议按这个顺序过一遍:
- 先量化:跑 cyclictest,记录 min、avg、max 和 99% 分布,确认延迟是普遍存在还是偶发尖峰。
- 检查亲和性:查看 RT 线程落在哪个 CPU,再对比 /proc/interrupts 里高频中断的 CPU 分布。
- 临时调整:用 taskset 或 pthread_setaffinity_np 固定线程,用 smp_affinity 把中断挪开,重点测试消除同核干扰。
- 确认 RT 预算:如果线程长时间运行占满 CPU,检查 /proc/sys/kernel/sched_rt_runtime_us 是否为默认 950000。
- 检查锁协议:对共享 mutex 使用 PTHREAD_PRIO_INHERIT,并确认阻塞点没有深入内核异步路径。
- 抓 trace:在延迟复现时用 ftrace 记录事件窗口,看最后一段是中断、软中断还是其他 RT 任务抢先。
- 固化并回归:把 isolcpus、threadirqs、IRQ 亲和性、RT 预算等写进启动参数或服务配置,避免内核参数改变后回归。
回到开头的问题:高优先级线程被延迟,往往不是调度器“不听话”,而是系统里有太多路径天然排在线程前面。硬中断、软中断、内核临界区、RT 带宽限制以及优先级反转,每一项单独看都只是几十微秒或一两百微秒,但叠加在一起,就形成了测得到的尾延迟。
RT 调度的本质,是让系统为关键线程让路,而不是给关键线程一张无限特权卡。把延迟测量和事件 trace 对应起来,比单纯追求更高优先级更能解决实际问题。如果你已经做完上述调整仍看到零星尖峰,也别急着下结论,回到 /proc/interrupts 和 ftrace 现场,往往能找到下一条没被覆盖的路径。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/1034/