这篇文章尝试阐明一条普遍存在于嵌入式实时系统(RTOS)中的bug, 包括ucosii, rt-thread, nuttx在内的大多数RTOS系统中都存在本文要说的问题.虽说是一条bug,但是在实际的项目应用中,只有很小概率会造成严重的后果,因为只有在比较苛刻的几个条件同时满足的时候,它才会表现的比较致命.但伟人墨菲曾经曰过 “如果觉得事件有可能发生,那它就一定会发生”,所以呢,为了保证有不幸遇到这个问题的同学能跨过这个坎儿,或者让大家装X的时候有货,我们就来分析一下这个问题。
首先从优先级反转讲起. 优先级反转
这个世界中,有些东西只属于某一个人,或者说在某一个时间段只能属于某个人,映射到多任务系统中,不同任务之间存在共享资源,操作系统一般会提供mutex等同步机制来保证数据同步.有时候低优先级的任务已经持有了某个共享资源,因此,如果一个高优先级的任务想要访问该共享资源,需要等待低优先级的任务释放该资源,这种高优先级等待低优先级完成后才可以执行的现象被称为优先级翻转(Priority Inversion).下图是一个发生优先级反转的例子,系统一开始任务 C 执行 P(S) 申请访问共享资源 S 并获得 S 的占有权,然后高优先级任务 A 被创建并调度执行,此时任务 A 也需要访问资源 S,但是因为 C 已经占有了资源 S,所以 A 被迫等待资源 S 直到任务 C执行 V(S) 释放资源 S。除非能保证所有任务在访问共享资源时,共享资源都未被其他低优先级的任务占有,否则为了保证数据同步,优先级反转的现象很难避免。一般能够接受系统出现下图中所示的情况,因为任务对共享资源的访问一般都会在较短时间结束,下图中任务 A 虽然在等待一个低优先级的任务 C,但等待的时间是有限的。因为我们基于这样一个前提,所有持有锁的任务对系统来说是有责任的,它必须保证创建的临界区最小,资源使用完毕,立即释放,如果线程没有意识到这种责任,比如在持有阻塞高优先级资源的锁临界区内去睡眠,这就是设计者的责任了.另外关于这个责任的问题,值得一讲的是,互斥量(mutex)和信号量(semaphore)的责任是不一样的,mutex具有属主属性,所以对它的要求更高,拥有mutex的任务更倾向于在尽可能短的时间内释放锁。而semaphore由于没有属主特性,释放锁的时限要求没有那么高。
下图给出了一个更糟糕的优先级反转的例子,假设任务优先级 A>B>C,一开始任务 C 执行 P(S) 申请访问共享资源 S 并获得 S 的占有权;然后任务 A 被创建并调度执行,此时任务 A 也需要访问资源 S,但是因为 C 已经占有了资源S,所以 A 被迫等待资源 S 直到任务 C 执行 V(S) 释放资源 S;但是 C 在执行时,任务 B 被创建并调度执行,此时任务 A 不仅仅需要等待任务 C 还需要等待任务 B,但任务 B 不在临界区中,所以执行时间是不确定的。这种情况对于实时系统来说是致命的,因为高优先级的任务 A 可能永远无法被调度执行,这种高优先级任务可能无限期等待低优先级任务执行的现象被称为无边界优先级反转(unbounded-priority-inversion)。
图中包含 4 个任务 A、B、C、D 以及两个互斥信号量 S1、S2。用户设置的原始优先级满足下述关系:PrD < PrC < PrS2 < PrB < PrA < PrS1。假设一开始任务 A、B、C 被延迟并且正在等待时钟中断唤醒,D 是唯一正在运行的任务。首先任务 D 获取信号量S2,然后 C 被唤醒并抢占了 D。C 接着获取了信号量 S1, 然后尝试获取 S2,但因为 S2 已经被 D 占有,所以 C 会被阻塞,因为 PrD < PrC,所以根据协议将D 的优先级提升到 PrS2,并且 D 被调度执行。然后任务 A 被唤醒,因为 A 的优先级最高,所以 A 抢占了 D。此时 A 尝试获取被 C 占有的 S1 ,所以 A 被阻塞,并且 C 的优先级被提升到 PrS1。最后任务 B 被唤醒并执行,但此时已经发生了无边界优先级反转,因为任务 A 可能无限期等待任务 B。并且从图中可以看出,此时任务 B 不占有任何资源,并且原始优先级低于 A。前者何时出让处理器,不可预期。图中括号内的二元组表示该任务当前优先级和状态.
从就绪队列的角度看,最高优先级的任务A的优先级,并没有传染给就绪队列的其他任务,因为虽然任务C受到了A的影响,被提升了优先级,但是由于任务C睡眠在了S2上.S1的优先级并没有间接影响到S2的owner任务,也就是D.导致无边界优先级反转的情况再次出现.
这个bug,在rt-thread操作系统中同样存在, 口说无凭,程序为证,下面这段程序没有用法上面的问题,但却可以把一个活活的RTT系统跑死(同样的逻辑我在Nuttx和UCOSII中都跑过,同样有问题,所以这个例子不能说明RTT有问题,只能说它follow的是通常的设计实现,RTT仍然是我向大家首推的系统).
这篇文章尝试阐明一条普遍存在于嵌入式实时系统(RTOS)中的bug, 包括ucosii, rt-thread, nuttx在内的大多数RTOS系统中都存在本文要说的问题.虽说是一条bug,但是在实际的项目应用中,只有很小概率会造成严重的后果,因为只有在比较苛刻的几个条件同时满足的时候,它才会表现的比较致命.但伟人墨菲曾经曰过 “如果觉得事件有可能发生,那它就一定会发生”,所以呢,为了保证有不幸遇到这个问题的同学能跨过这个坎儿,或者让大家装X的时候有货,我们就来分析一下这个问题。
首先从优先级反转讲起. 优先级反转
这个世界中,有些东西只属于某一个人,或者说在某一个时间段只能属于某个人,映射到多任务系统中,不同任务之间存在共享资源,操作系统一般会提供mutex等同步机制来保证数据同步.有时候低优先级的任务已经持有了某个共享资源,因此,如果一个高优先级的任务想要访问该共享资源,需要等待低优先级的任务释放该资源,这种高优先级等待低优先级完成后才可以执行的现象被称为优先级翻转(Priority Inversion).下图是一个发生优先级反转的例子,系统一开始任务 C 执行 P(S) 申请访问共享资源 S 并获得 S 的占有权,然后高优先级任务 A 被创建并调度执行,此时任务 A 也需要访问资源 S,但是因为 C 已经占有了资源 S,所以 A 被迫等待资源 S 直到任务 C执行 V(S) 释放资源 S。除非能保证所有任务在访问共享资源时,共享资源都未被其他低优先级的任务占有,否则为了保证数据同步,优先级反转的现象很难避免。一般能够接受系统出现下图中所示的情况,因为任务对共享资源的访问一般都会在较短时间结束,下图中任务 A 虽然在等待一个低优先级的任务 C,但等待的时间是有限的。因为我们基于这样一个前提,所有持有锁的任务对系统来说是有责任的,它必须保证创建的临界区最小,资源使用完毕,立即释放,如果线程没有意识到这种责任,比如在持有阻塞高优先级资源的锁临界区内去睡眠,这就是设计者的责任了.另外关于这个责任的问题,值得一讲的是,互斥量(mutex)和信号量(semaphore)的责任是不一样的,mutex具有属主属性,所以对它的要求更高,拥有mutex的任务更倾向于在尽可能短的时间内释放锁。而semaphore由于没有属主特性,释放锁的时限要求没有那么高。
下图给出了一个更糟糕的优先级反转的例子,假设任务优先级 A>B>C,一开始任务 C 执行 P(S) 申请访问共享资源 S 并获得 S 的占有权;然后任务 A 被创建并调度执行,此时任务 A 也需要访问资源 S,但是因为 C 已经占有了资源S,所以 A 被迫等待资源 S 直到任务 C 执行 V(S) 释放资源 S;但是 C 在执行时,任务 B 被创建并调度执行,此时任务 A 不仅仅需要等待任务 C 还需要等待任务 B,但任务 B 不在临界区中,所以执行时间是不确定的。这种情况对于实时系统来说是致命的,因为高优先级的任务 A 可能永远无法被调度执行,这种高优先级任务可能无限期等待低优先级任务执行的现象被称为无边界优先级反转(unbounded-priority-inversion)。
图中包含 4 个任务 A、B、C、D 以及两个互斥信号量 S1、S2。用户设置的原始优先级满足下述关系:PrD < PrC < PrS2 < PrB < PrA < PrS1。假设一开始任务 A、B、C 被延迟并且正在等待时钟中断唤醒,D 是唯一正在运行的任务。首先任务 D 获取信号量S2,然后 C 被唤醒并抢占了 D。C 接着获取了信号量 S1, 然后尝试获取 S2,但因为 S2 已经被 D 占有,所以 C 会被阻塞,因为 PrD < PrC,所以根据协议将D 的优先级提升到 PrS2,并且 D 被调度执行。然后任务 A 被唤醒,因为 A 的优先级最高,所以 A 抢占了 D。此时 A 尝试获取被 C 占有的 S1 ,所以 A 被阻塞,并且 C 的优先级被提升到 PrS1。最后任务 B 被唤醒并执行,但此时已经发生了无边界优先级反转,因为任务 A 可能无限期等待任务 B。并且从图中可以看出,此时任务 B 不占有任何资源,并且原始优先级低于 A。前者何时出让处理器,不可预期。图中括号内的二元组表示该任务当前优先级和状态.
从就绪队列的角度看,最高优先级的任务A的优先级,并没有传染给就绪队列的其他任务,因为虽然任务C受到了A的影响,被提升了优先级,但是由于任务C睡眠在了S2上.S1的优先级并没有间接影响到S2的owner任务,也就是D.导致无边界优先级反转的情况再次出现.
这个bug,在rt-thread操作系统中同样存在, 口说无凭,程序为证,下面这段程序没有用法上面的问题,但却可以把一个活活的RTT系统跑死(同样的逻辑我在Nuttx和UCOSII中都跑过,同样有问题,所以这个例子不能说明RTT有问题,只能说它follow的是通常的设计实现,RTT仍然是我向大家首推的系统).