ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

高优先级线程越多越好吗?实时调度中的优先级与资源竞争

高优先级线程越多越好吗?实时调度中的优先级与资源竞争

在工业控制、机器人运动控制和实时数据采集系统中,为了让关键任务及时运行,工程师通常会使用 Linux 的实时调度策略,例如SCHED_FIFO和SCHED_RR,并为控制线程设置较高的实时优先级。

但在实际项目中,有一种看似合理、实际上可能适得其反的做法:只要某个线程比较重要,就提高它的优先级;只要某个任务可能影响实时性,就把它也设置成高优先级。随着系统功能逐渐增加,实时线程的数量越来越多,优先级也越来越高,最终反而出现任务饥饿、锁等待、周期超时和系统响应异常等问题。

原因在于,优先级并不是一种能够凭空增加 CPU 计算能力的资源。它决定的是符合调度条件的任务之间如何竞争 CPU。当多个线程同时处于可运行状态时,提高某个线程的优先级,往往意味着其他线程获得 CPU 的机会减少。如果优先级分配缺乏明确的任务模型,系统就可能把资源集中给某些线程,却让它们依赖的辅助任务无法及时执行。

例如,一个机器人控制系统包含 EtherCAT 周期任务、运动学计算、传感器处理、状态监控、日志记录和网络通信。如果所有线程都设置为高优先级,系统并不会因此让所有任务同时运行。相反,高优先级线程可能不断抢占其他线程,导致低优先级数据处理任务无法及时更新共享状态;控制线程随后又等待这些状态,最终形成资源竞争和任务延迟。

因此,实时调度的核心不是尽可能提高所有任务的优先级,而是根据任务周期、截止时间、执行时间、资源依赖和系统负载,设计一套能够被验证的调度关系。本文从 Linux 实时调度机制出发,分析优先级分配中的常见误区,并介绍如何在工业控制系统中建立合理的实时调度方案。

一、SCHED_FIFO 和 SCHED_RR 到底怎样决定线程的执行顺序?

在讨论优先级规划之前,需要先理解 Linux 中常见的实时调度策略。

对于使用SCHED_FIFO的线程,调度器会优先选择当前可运行的最高优先级实时线程。在同一优先级的普通 FIFO 竞争关系中,线程通常持续运行,直到主动让出 CPU、进入阻塞状态、被更高优先级线程抢占,或者受到其他调度规则影响。它不会因为消耗了一个普通的时间片,就像普通公平调度任务那样自动轮换。

这意味着,如果一个高优先级SCHED_FIFO线程始终处于可运行状态,却没有正确阻塞、主动让出 CPU 或结束本次工作,那么较低优先级的线程可能长期得不到运行机会。

下面是一个简单的场景:

  • 线程 A:实时优先级 90,采用SCHED_FIFO。

  • 线程 B:实时优先级 70,采用SCHED_FIFO。

  • 线程 C:普通调度策略,执行日志和监控任务。

如果线程 A 持续占用 CPU,线程 B 和线程 C 就可能无法及时运行。即使线程 B 负责准备下一周期所需的数据,也不能仅凭它的业务重要性保证它得到 CPU。调度器主要依据实际调度属性和可运行状态进行决策,而不是自动理解应用程序中的业务依赖。

因此,SCHED_FIFO适合那些执行路径清晰、时间预算明确、能够合理阻塞或完成工作的实时任务,但不适合没有明确执行边界、可能无限循环占用 CPU 的线程。

SCHED_RR则在相同实时优先级的可运行线程之间引入轮转时间片。一个线程使用完相应的轮转时间后,可以被安排到同优先级队列中的其他线程之后,让同级线程获得运行机会。

不过,轮转时间片并不意味着SCHED_RR会自动公平地分配所有 CPU 时间。不同优先级之间仍然存在抢占关系:低优先级线程不会仅仅因为等待时间较长,就自动越过持续可运行的高优先级实时线程。

因此,两种策略的主要区别在于同优先级实时线程之间的运行方式,而不是简单的“一个实时性更高、另一个实时性更低”。

在实际控制系统中,选择哪种策略,应根据线程是否需要持续执行、同级任务之间是否需要轮转,以及任务的时间约束决定。对于短周期控制线程,常见做法是优先分析SCHED_FIFO是否适合其执行模型;如果存在多个同级实时工作线程,也应评估SCHED_RR的轮转行为对任务截止时间的影响。

需要注意,Linux 实时优先级的可用范围和配置还受到系统实现、权限、资源限制及相关配置影响。不能仅凭一个优先级数值就判断任务一定能够按时运行。

二、为什么高优先级线程太多,反而可能让系统更不稳定?

优先级分配的首要问题,是多个线程之间存在竞争,而系统的 CPU 时间是有限的。

假设一个控制系统有四个实时线程:

线程功能周期单次执行时间
A读取传感器状态1 ms100 μs
B运动控制计算1 ms250 μs
C更新执行器输出1 ms100 μs
D状态监控与数据整理5 ms400 μs

以上数值只是用于说明调度关系的假设数据,不代表任何实测平台结果。

如果 A、B、C、D 都设置成很高的实时优先级,而且没有根据截止时间和数据依赖安排优先级,那么 D 可能在某个时刻抢占 B,导致控制计算延迟;也可能出现 B 已经完成计算,却因为 C 依赖的某项数据尚未准备好而等待。

如果 D 实际上只是用于整理监控数据,那么将它与关键控制线程设置成相同甚至更高的优先级,通常需要非常明确的工程理由。否则,它会把 CPU 资源分配给相对不紧急的工作,增加关键任务的调度竞争。

这里需要区分两个问题:任务是否重要,与任务是否必须立即运行,并不是同一回事。

日志记录可能对故障分析非常重要,但大部分日志处理不需要在每个控制周期内立即完成。网络状态上报可能关系到系统可观测性,但如果它不是当前控制闭环的一部分,就不应轻易抢占周期控制任务。相反,如果某个传感器采样线程的输出直接决定下一次控制计算,那么它的调度时机就可能具有更严格的约束。

因此,优先级规划应基于任务的时间要求,而不是单纯根据业务重要程度排序。

另一个常见问题是高优先级任务的 CPU 需求超过了系统能够提供的时间预算。

例如,某个 CPU 上有两个高优先级周期任务,二者的周期分别为 1 ms 和 2 ms,执行时间分别为 700 μs 和 800 μs。若它们必须在同一个 CPU 上运行,并且这些执行时间是每次都需要的工作量,那么仅从长期平均 CPU 需求看:

  • 第一个任务的利用率为 700 μs / 1000 μs,即 70%。

  • 第二个任务的利用率为 800 μs / 2000 μs,即 40%。

两者合计达到 110%。这意味着长期所需的 CPU 时间已经超过单核能够提供的时间,即使优先级分配得再合理,也无法让这两个任务始终按时完成。

这个例子说明,优先级不能替代可调度性分析。即使系统整体 CPU 使用率很低,如果关键任务被限制在同一个核心上,或者某些任务必须串行执行,局部 CPU 时间仍然可能不足。

当然,对于具有不同截止时间、阻塞关系、释放模式和抢占规则的复杂任务集,仅用平均利用率并不能完整判断系统是否可调度。还需要分析任务释放时间、执行时间上界、优先级关系、阻塞时间和截止时间,并在实际平台上进行验证。

对于实时系统来说,提高优先级不能解决计算资源不足,也不能让超出时间预算的任务自动变得可调度。

三、优先级反转与资源依赖:为什么关键线程可能被低优先级任务拖住?

实时调度中最容易被误判的问题之一,是优先级反转。

假设一个系统有三个线程:高优先级控制线程 H、中优先级业务线程 M 和低优先级辅助线程 L。

线程 L 负责更新共享状态,并在临界区内持有互斥锁。线程 H 需要获取同一把锁才能执行本周期的控制计算。当 H 试图获取锁时,由于 L 尚未释放锁,H 进入等待状态。

此时,线程 M 恰好处于可运行状态。如果调度关系允许 M 抢占 L,那么 L 就无法及时完成临界区,H 也只能继续等待。

最终的结果是:一个低优先级线程间接决定了高优先级控制线程何时能够继续运行。这就是优先级反转的典型表现。

在这个场景中,简单地进一步提高 H 的优先级,通常并不能直接消除根因,因为 H 已经由于锁竞争而阻塞。它不能越过互斥锁直接访问被保护的数据。

可以从三个方面改善这种问题。

首先,缩短临界区。不要让低优先级线程持有共享锁时执行文件 I/O、网络通信、复杂计算或其他可能长时间阻塞的操作。临界区越短,潜在的锁等待时间通常越容易控制。

其次,评估优先级继承等同步机制。在 Linux 支持相应协议的情况下,可以使用具有优先级继承能力的互斥锁,使持锁线程在特定竞争情况下临时继承等待线程的优先级,减少中优先级任务对锁释放的干扰。

但优先级继承并不等于锁等待时间为零。持锁线程仍然需要完成临界区工作,如果临界区本身执行很久,或者存在复杂的多锁依赖,等待仍可能影响截止时间。优先级继承也不是所有同步原语都默认提供的能力,应用必须检查实际配置与平台支持情况。

第三,减少关键控制路径对共享锁的依赖。对于一些状态交换场景,可以评估使用固定容量队列、双缓冲、不可变数据快照或其他适合数据访问模式的机制。

例如,非实时线程负责准备一份新的配置快照,控制线程只读取已经发布的有效版本。这样可以避免控制线程为了读取配置而等待一个执行复杂操作的业务线程。但这种设计需要明确数据一致性、缓冲区生命周期和更新时机,不能只因为使用了原子变量或双缓冲,就假定整个数据交换过程不存在竞争。

除了互斥锁,资源依赖还可能来自其他地方,例如:

  • 控制线程等待传感器线程准备输入数据。

  • 运动控制任务等待通信线程完成数据交换。

  • 实时线程等待设备驱动返回结果。

  • 高优先级任务等待一个由普通业务线程填充的队列。

  • 多个实时线程在周期边界同时访问共享状态。

这些场景不一定都表现为传统意义上的优先级反转,但它们都会影响实际任务的完成时间。

因此,设计实时调度策略时,不能只看每个线程的优先级,还必须画清楚线程之间的数据依赖与阻塞关系。一个高优先级任务如果依赖多个低优先级任务,那么这些依赖路径就需要纳入整体时间预算。

四、怎样为实时线程分配优先级?从截止时间与任务模型出发

合理的优先级设计应当先建立任务模型,而不是先给线程分配一个看起来足够高的数值。

至少需要明确以下信息:

  1. 任务的释放模式:周期性、事件触发,还是混合模式。

  2. 任务的周期或最小到达间隔。

  3. 任务每次执行所需的 CPU 时间,以及在目标平台上的变化范围。

  4. 任务的相对截止时间。

  5. 任务是否会阻塞,以及可能等待哪些共享资源。

  6. 任务与其他线程之间的数据依赖关系。

  7. 任务运行在哪个 CPU,以及是否会受到其他任务和中断的影响。

这里尤其需要区分任务周期与截止时间。周期为 1 ms 的任务,不代表它一定有完整的 1 ms 可以执行。如果它必须在下一次控制更新之前完成,而执行链路中还包含数据交换和输出提交,那么真正留给计算步骤的时间可能明显小于 1 ms。

方法一:先根据截止时间建立任务优先级关系

对于某些独立的、周期性、可抢占任务集,如果任务执行时间和截止时间满足特定假设,可以使用经典的固定优先级调度分析方法。一个常见思路是根据任务的周期或截止时间安排相对优先级。

但这种理论方法有适用条件,不能直接套用到所有工业控制程序。实际任务可能存在资源共享、非周期事件、不同截止时间、驱动阻塞和跨线程同步,因此需要根据具体任务模型选择分析方法。

工程上,可以先明确哪些任务直接参与关键控制闭环,哪些任务属于辅助工作,再结合周期、截止时间和数据依赖决定优先级,而不是把所有线程都设置为同一级别。

方法二:分析 CPU 时间预算

对于每个 CPU 上的任务,应估算它们在一个时间窗口内需要多少计算时间,并将任务阻塞、抢占以及内核和中断活动纳入分析。

对于周期任务,一个初步指标是 CPU 利用率:

[
U_i=\frac{C_i}{T_i}
]

其中,(C_i) 表示任务每次执行需要的 CPU 时间,(T_i) 表示任务周期。

如果同一个 CPU 上有多个周期任务,可以先计算:

[
U_{\mathrm{total}}=\sum_i \frac{C_i}{T_i}
]

这个总利用率有助于发现明显的资源不足问题,但它不是任意实时任务集都能满足截止时间的充分条件。任务释放时刻、截止时间、优先级、阻塞、抢占以及中断开销,都可能改变实际调度结果。

因此,利用率分析应该作为初步筛查,而不是最终验收结论。

方法三:明确实时任务与辅助任务的边界

在工业控制系统中,建议优先识别真正影响控制闭环的任务。例如,周期性运动控制计算、必要的数据采集以及必须按时完成的输出更新,可能属于关键路径。

而日志格式化、历史数据整理、界面刷新、非关键网络上报和报表生成等工作,通常可以安排在较低优先级或普通调度线程中。但是否能够降低优先级,仍需确认这些任务没有承担关键的实时依赖。

例如,如果日志线程还承担实时故障信号的唯一转发职责,那么不能在不了解其功能的情况下直接降低其优先级。更好的设计通常是将实时故障检测和必要的安全响应与后续日志存储、网络上报拆分,让必须及时完成的动作具有明确的执行路径。

方法四:用测量结果修正任务模型

任务模型不能只依靠源代码阅读或开发人员估计。应当通过应用埋点、调度跟踪和压力测试,记录每个关键任务的实际启动延迟、执行时间、阻塞时间及完成情况。

如果某个任务偶尔执行时间明显增加,需要进一步分析是计算量变化、缓存和内存访问、锁等待、设备调用,还是被其他任务抢占。随后再决定是否需要调整优先级、改变 CPU 绑定方式或者优化执行路径。

优先级调整应当建立在可解释的原因之上。盲目把某个线程提高一级,可能只是让其他线程的问题变得更加明显,并没有消除根本原因。

五、在 Linux 中怎样检查调度配置并验证优先级方案?

在完成初步任务分析之后,可以从当前运行状态入手,检查线程的调度策略、优先级和 CPU 分布。

首先,查看所有线程的调度信息:

ps -eLo pid,tid,psr,cls,rtprio,pri,ni,stat,comm

结合输出确认关键控制线程的 TID、调度类别、实时优先级和最近运行的 CPU。对于多线程程序,不要只检查进程主线程,因为真正执行周期控制工作的可能是单独创建的线程。

接着,可以使用chrt查询或调整指定线程的实时调度属性。例如:

chrt -p <TID>

这条命令用于查询目标线程的调度策略与相关优先级信息。修改实时优先级时,需要具备相应权限,并确认系统资源限制和平台策略允许该操作。

在生产环境中,更推荐通过程序初始化代码统一设置线程的调度属性,并检查每次配置操作的返回结果。不能假定请求了实时策略就一定设置成功,也不能忽略线程创建、继承属性和系统资源限制造成的差异。

如果怀疑线程之间存在调度等待,可以进一步采集调度事件。例如:

sudo trace-cmd record \ -e sched:sched_wakeup \ -e sched:sched_switch \ sleep 10 sudo trace-cmd report

实际使用时,应确认当前内核支持相关事件,并尽量通过目标 TID 过滤记录,避免采集过多无关数据。跟踪开销也可能影响被测系统,因此应在测试环境中验证采集方式,并控制记录时长和缓冲区大小。

分析时,重点关注以下情况:

  • 目标线程已经被唤醒,却没有立即获得 CPU。

  • 高优先级线程持续运行,其他任务长时间处于可运行状态。

  • 目标线程运行后又进入阻塞,等待共享锁或其他资源。

  • 某个低优先级线程持有共享资源,导致高优先级线程无法继续执行。

  • 多个实时线程集中运行在同一 CPU,局部计算需求超过该核心的时间预算。

调度跟踪只能提供内核调度行为的证据,不能单独解释所有用户态计算和设备等待。因此,建议在关键代码路径中添加时间戳,记录任务计划释放时间、实际开始时间、关键步骤耗时和最终完成时间,再将这些数据与内核事件对应起来。

最后,使用相同硬件、内核、驱动、业务负载和测试时长,对不同优先级方案进行对照。除了平均延迟,还应记录 P95、P99、P99.9、最大观测延迟、样本数量和截止时间违约次数。若测试过程中没有发生超时,应明确测试持续时间和样本规模,不能把有限时间内未观察到超时理解为任何条件下都不会超时。

对于望获OS或其他实时 Linux 平台,同样应通过统一的任务模型和测试口径验证优先级配置。系统平台能够提供调度能力,但具体应用是否满足实时要求,仍取决于任务设计、资源依赖、CPU 分配和实际负载。

总结来说,实时调度不是一场不断提高优先级数值的竞赛。优先级的作用是协调任务竞争,而不是创造额外的计算资源。只有先明确任务周期、截止时间、执行时间和资源依赖,再合理安排优先级、减少关键路径阻塞并通过压力测试验证,才能建立真正可靠的实时调度方案。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进