尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

实时Linux为什么会出现优先级反转?从Priority Inversion理解实时系统中的锁与同步

实时Linux为什么会出现优先级反转?从Priority Inversion理解实时系统中的锁与同步 上一篇文章我们讨论了SCHED_FIFO、SCHED_RR和SCHED_DEADLINE三种Linux实时调度策略。理解这些调度策略之后还会遇到一个非常容易让人困惑的问题如果高优先级任务本来就应该优先运行那么为什么它有时候还是会被低优先级任务“卡住”这听起来似乎违反了实时调度的基本逻辑。假设系统中存在三个任务高优先级任务 H 中优先级任务 M 低优先级任务 L按照正常的实时调度逻辑只要H进入可运行状态它就应该抢占M和L。但是如果H需要访问某个共享资源而这个资源恰好被L持有那么H就不得不等待L释放资源。事情到这里其实还不算严重。真正的问题是当L正在准备释放资源时M突然开始运行并且M又抢占了L。于是系统出现了一条非常奇怪的执行链H等待L L被M抢占 M继续运行 L无法释放资源 H持续等待最终的结果是高优先级任务H实际上因为低优先级任务L而无法继续执行。这就是实时系统中的经典问题——Priority Inversion优先级反转。它不是某个Linux特有的问题而是实时操作系统、嵌入式系统、多任务系统中非常典型的并发与同步问题。更重要的是优先级反转说明了一件非常关键的事情实时性不只是调度器决定“谁先运行”还取决于任务之间如何共享资源。因此当我们从PREEMPT_RT一路讨论到实时调度再进入优先级反转时其实已经从“调度问题”进入了实时操作系统更深的一层任务之间如何安全、及时、可预测地协同执行。一、什么是优先级反转为什么高优先级任务反而可能被低优先级任务阻塞理解优先级反转最简单的方法就是从一个具体场景开始。假设一个控制系统里存在三个任务H高优先级控制任务 M中优先级计算任务 L低优先级后台任务优先级关系H M L正常情况下系统应该是H运行 ↓ H阻塞 ↓ M运行 ↓ M阻塞 ↓ L运行也就是说优先级越高的任务在可运行状态下越容易获得CPU。现在增加一个共享资源。例如三个任务都需要访问某个共享数据结构Shared Resource ↑ ┌───┼───┐ H M L为了保证数据一致性程序使用互斥锁lock(shared_resource); 访问共享数据 unlock(shared_resource);这时候事情就开始发生变化了。假设L首先运行并且成功获得锁L lock() ↓ 持有锁此时H突然进入运行状态。因为H的优先级最高所以H立即抢占LH → 运行 L → 被抢占但是H执行过程中也需要访问Shared Resource。于是H执行lock(shared_resource);结果发现锁已经被L持有因此H不能继续执行只能进入等待状态。此时CPU可以去运行其他任务。假设M也处于Runnable状态H等待锁 MRunnable L持有锁因为M L所以M可以抢占L。最终执行顺序就变成L获得锁 ↓ H到达抢占L ↓ H等待L释放锁 ↓ M抢占L ↓ M继续运行 ↓ L无法获得CPU ↓ L无法释放锁 ↓ H继续等待注意这里最关键的一点。H本来是系统中最高优先级的任务。但是现在H ← 等待L L ← 被M抢占 M ← 持续运行于是实际上形成H ← L ← M也就是说H的执行时间被L间接影响而L又被M影响。这就是优先级反转最经典的表现。从表面上看系统仍然严格按照优先级调度。问题恰恰在于调度器只知道任务的优先级却不知道“L持有的这把锁实际上正在阻塞H”。因此如果系统没有额外的实时同步机制优先级体系本身并不能保证任务拥有确定的响应时间。这也是为什么实时系统不能只研究SCHED_FIFO SCHED_RR SCHED_DEADLINE还必须研究Mutex Semaphore Spinlock Priority Inheritance Priority Ceiling等同步机制。对于普通应用来说锁主要解决的是多线程同时访问共享资源时如何避免数据竞争。而对于实时系统来说锁还需要解决另一个问题一个任务等待锁的最长时间到底是多少这就是实时系统与普通多线程系统之间非常重要的区别。普通程序可能只关心最终能不能拿到锁实时程序还需要关心最坏情况下多久能拿到锁因为对于一个周期为1ms的控制任务而言如果锁竞争让它随机等待几十微秒、几百微秒甚至更长时间那么最终产生的就不只是“程序慢了一点”而是控制周期的抖动。二、为什么优先级反转会破坏实时性真正危险的是“不可预测的等待时间”优先级反转真正严重的地方并不是“高优先级任务等待低优先级任务”。实际上在实时系统里高优先级任务等待资源本身并不是绝对不能接受的。真正的问题是等待时间是否可以被控制和分析。假设H需要等待L释放锁。如果能够明确知道L最多还需要执行20μs那么系统设计者完全可以把这20μs纳入实时预算。但是如果L的执行过程中还可能受到M、N、P等大量任务影响那么H的等待时间就会变成20μs 100μs 500μs 1ms这时候实时系统最怕的事情就出现了最坏情况无法确定。假设控制任务周期T 1ms正常情况下执行时间 100μs系统看起来完全没有问题。但是某次执行过程中发生锁竞争100μs 锁等待300μs 其他内核活动100μs 中断80μs那么最终可能变成580μs如果继续叠加其他干扰甚至可能超过整个控制周期。因此实时系统评价性能时不能只看平均延迟 平均执行时间 平均CPU利用率还必须关注Worst-case latency Worst-case execution time Worst-case blocking time也就是最坏情况延迟、最坏情况执行时间、最坏情况阻塞时间。这也是为什么前面讨论CPU Isolation时一直强调“确定性”。核心隔离的意义之一就是尽可能减少来自其他任务的CPU竞争。而实时锁的意义则是尽可能控制任务因为共享资源产生的阻塞时间。两者解决的是不同层面的问题。例如CPU Isolation → 减少CPU资源竞争 实时调度 → 决定任务运行顺序 优先级继承 → 处理高低优先级任务之间的锁竞争 PREEMPT_RT → 缩短内核不可抢占路径及部分同步机制带来的实时阻塞把这些技术放在一起看实时系统的结构就越来越清晰。三、Priority Inheritance是什么为什么让低优先级任务“临时变高”可以解决问题解决优先级反转最经典的方法之一就是Priority Inheritance优先级继承。简称PI。它的核心思想非常简单如果高优先级任务正在等待低优先级任务持有的锁那么暂时提高低优先级任务的有效优先级让它尽快完成临界区并释放锁。还是使用前面的例子H M LL首先获得锁L 持有LockH到达H 请求Lock 发现Lock被L持有 开始等待如果没有优先级继承L低优先级 M中优先级 H高优先级等待M仍然可以抢占L。但如果使用Priority InheritanceH等待L持有的Lock ↓ L继承H的优先级 ↓ L临时获得接近H的调度优先级 ↓ M无法轻易抢占L ↓ L快速完成临界区 ↓ L释放Lock ↓ H获得Lock整个过程可以画成H │ │ 等待Lock ▼ L ← 持有Lock │ │ 继承H优先级 ▼ 快速完成临界区 │ │ unlock() ▼ H继续运行这时候原来的H ← L ← M变成H等待L L暂时提升 L快速释放锁 H继续运行M就不会因为优先级高于L而无限期地干扰L。注意一个非常容易理解错误的地方优先级继承并不是永久改变L的优先级。L本来的优先级仍然是低优先级。只是因为它当前持有的锁正在阻塞一个高优先级任务所以系统暂时提高L的有效调度优先级。当L释放锁之后它就可以恢复原来的优先级。因此优先级继承可以理解成让“真正阻塞高优先级任务的人”获得足够的CPU时间尽快完成导致阻塞的那段工作。这是一种非常典型的实时系统设计思想。普通调度逻辑是谁优先级高谁先运行。实时同步机制进一步考虑谁正在阻塞关键任务谁就应该尽快获得资源。两者结合起来才能真正建立可预测的实时任务执行环境。当然实际系统中的优先级继承并没有这么简单。如果一个低优先级任务同时持有多个锁L ├── Lock A ├── Lock B └── Lock C而不同的高优先级任务又分别等待这些锁H1 → Lock A H2 → Lock B H3 → Lock C那么优先级继承就会变成一个更加复杂的动态过程。如果多个任务之间形成锁依赖A → B → C甚至可能出现优先级链式传播。因此一个成熟的实时操作系统不仅需要调度器还需要完整的实时同步机制。四、PREEMPT_RT为什么特别关注锁实时化Linux并不是简单地“把抢占打开”说到这里就可以重新理解前面文章讨论过的PREEMPT_RT。很多人第一次接触PREEMPT_RT时会把它简单理解成“让Linux内核变得可以抢占。”这个理解并不完整。因为Linux内核中存在大量同步机制。例如mutex spinlock rwlock semaphore普通Linux环境下一些锁的设计目标主要是保护共享数据 提高并发性能 降低锁开销但是到了实时系统中还必须增加一个要求锁不能给实时任务制造无法控制的长时间阻塞。尤其是在内核中。如果一个实时任务因为进入内核后遇到了一个不可预测的长时间锁竞争那么即使用户态任务使用了SCHED_FIFO实时响应也可能受到影响。因此PREEMPT_RT的价值并不能简单概括为“增加抢占”。它涉及Linux内核实时化过程中大量与锁 中断 调度 抢占 线程化 同步相关的机制调整。其中一个重要方向就是让部分原本难以进行实时抢占和优先级管理的内核执行路径更适合实时调度。例如Linux中很多中断处理传统上存在Hard IRQ在实时化过程中一部分中断处理工作可以通过线程化方式纳入更可控的调度体系。这样做的一个重要意义就是中断处理也可以更明确地参与优先级管理。假设实时控制任务Priority 90 普通设备中断Priority 50如果设备中断相关工作长期以不可控方式占用CPU那么即使控制任务优先级很高也可能受到影响。而通过更加实时化的内核机制可以进一步让系统对谁应该运行 什么时候可以被抢占 谁应该获得更高优先级进行更加细致的控制。这也是为什么实时Linux最终一定会进入“内核同步机制”这个问题。因为调度器决定谁想运行。锁决定谁能继续运行。这两者之间必须协同。如果只解决调度而不解决同步那么实时性仍然是不完整的。进一步来看PREEMPT_RT与Priority Inheritance之间也存在很强的联系。实时系统需要避免这样的情况高优先级任务 ↓ 等待锁 ↓ 低优先级任务 ↓ 被中优先级任务不断抢占 ↓ 高优先级任务长期等待而实时同步机制希望把它变成高优先级任务 ↓ 等待锁 ↓ 低优先级任务继承优先级 ↓ 快速执行临界区 ↓ 释放锁 ↓ 高优先级任务继续运行这样才能让阻塞时间变得更加可分析。所以当我们评价一个实时Linux系统时仅仅问有没有PREEMPT_RT其实还不够。还需要继续问实时锁如何处理 优先级继承是否支持 中断如何线程化 IRQ如何隔离 实时任务如何调度 CPU核心如何隔离 内核线程如何管理这就是为什么一个成熟的实时系统最终会从单点技术走向系统工程。五、从优先级反转看实时系统为什么“调度 隔离 同步”缺一不可到了这里可以把前面整个系列的技术路线重新串起来。最开始我们讨论的是普通Linux为什么不能天然满足所有实时场景答案是普通Linux的核心设计目标并不是让所有任务都具备严格的确定性响应。然后我们讨论PREEMPT_RT到底改变了什么答案是通过内核抢占、线程化中断以及实时同步等机制减少影响实时响应的内核路径。接着讨论CPU绑核为什么不等于核心隔离因为绑核 → 决定任务可以在哪些CPU上运行 核心隔离 → 尽可能减少指定CPU上的其他系统活动然后上一篇进一步讨论SCHED_FIFO、SCHED_RR、SCHED_DEADLINE有什么区别它们解决的是实时任务之间如何获得CPU而这一篇讨论优先级反转则进入了下一层实时任务获得CPU之后 为什么还可能因为共享资源而无法继续执行答案就是同步机制可能形成阻塞。所以一个完整的实时执行链条可以概括为实时任务 │ ▼ 实时调度策略 FIFO / RR / DEADLINE │ ▼ Linux调度器 │ ▼ PREEMPT_RT │ ┌────────┴────────┐ ▼ ▼ 中断控制 实时锁 │ │ ▼ ▼ IRQ隔离 优先级继承 │ │ └────────┬────────┘ ▼ CPU隔离 │ ▼ 确定性执行环境这里面任何一层出现严重问题都可能导致实时任务的最坏情况延迟扩大。例如只有SCHED_FIFO没有CPU隔离实时任务虽然拥有高优先级但CPU上仍然可能存在大量系统活动。只有CPU隔离没有实时调度CPU虽然比较干净但任务之间仍然可能缺乏合理的实时调度关系。只有实时调度和CPU隔离没有实时同步机制高优先级任务仍然可能因为锁竞争等待低优先级任务。只有PREEMPT_RT没有合理的应用设计应用本身如果存在过长临界区、不可控I/O、动态内存操作或者不合理任务模型仍然可能产生明显的实时抖动。所以真正成熟的实时系统并不是Linux PREEMPT_RT这么简单。更准确的理解应该是实时内核 实时调度 CPU资源隔离 中断资源隔离 实时同步 应用任务设计共同构成一个完整的实时执行环境。这也是望获OS相关技术体系值得关注的地方。对于工业控制、机器人、智能制造、边缘计算以及其他对确定性执行有要求的嵌入式场景来说用户真正需要的并不是一个“看起来实时”的Linux而是一套能够从调度 ↓ CPU ↓ 中断 ↓ 内核 ↓ 同步 ↓ 应用进行整体控制的实时系统。尤其是在一个系统同时运行普通Linux应用和关键实时任务时“资源隔离”的价值会进一步体现出来。例如可以把系统划分为普通计算域 ├── 网络 ├── 日志 ├── UI ├── 文件系统 └── AI计算 实时计算域 ├── 运动控制 ├── 实时采集 ├── 安全监测 └── 实时通信然后通过CPU核心隔离、任务调度、IRQ管理以及同步机制让两个区域之间尽可能减少相互干扰。这样Linux就不再只是一个“运行应用程序的平台”而开始成为一个可以同时承载不同实时等级任务的系统基础。这也是未来实时Linux非常重要的发展方向从单纯追求低延迟走向可预测、可隔离、可分析的确定性执行环境。而优先级反转恰恰告诉我们一个非常重要的事实实时性从来不是简单地给任务一个更高的优先级。真正的实时系统需要同时解决三个问题第一谁应该先运行 —— 实时调度 第二谁能够真正获得CPU —— CPU与系统资源隔离 第三谁正在阻塞关键任务 —— 实时同步与优先级继承只有这三个问题同时得到解决实时任务的最坏情况执行时间和最坏情况响应时间才有可能被真正控制。从这个角度看SCHED_FIFO、SCHED_RR、SCHED_DEADLINE、PREEMPT_RT、CPU Isolation以及Priority Inheritance并不是彼此独立的几个Linux技术名词。它们实际上共同组成了一条完整的实时技术链调度 → 抢占 → 隔离 → 同步 → 确定性而这条技术链也正是理解国产实时Linux和嵌入式硬实时操作系统的重要入口。下一篇可以继续往下深入一个更工程化的问题《实时Linux为什么还需要核心隔离从“实时任务”和“普通任务”共存看确定性系统设计》或者进一步进入混合关键性系统《一个Linux内核能不能同时跑AI和硬实时控制从混合关键性系统理解实时Linux架构》从这里开始就可以把前面几篇关于调度、锁、核心隔离的知识真正串成一套完整的实时Linux系统架构方法论。
返回列表