揭秘!基于RT-Thread探究“优先级反转”下的任务调度究竟是什么样的?| 技术集结

科技时尚 2025-08-19 荣骊达人 4301

本文将基于RT-Thread,结合 RT-Trace 调试器细化到实际任务调度的粒度,来调试并逐步讲解“优先级反转”的调度和运行逻辑。如果对 RT-Trace 感兴趣的可以看这篇文章:

国产嵌入式调试器之光? RT-Trace 初体验!

废话不多说,我们直接开始。本文基于 RT-Thread 来编写测试代码。在此之前我们先捋一下代码流程:

优先级反转问题的本质是高优先级任务因等待低优先级任务释放资源而被阻塞,同时又有其他中优先级任务抢占了 CPU,使得低优先级任务得不到执行机会,进而导致高优先级任务长时间无法继续运行。

因此我们需要建立三个不同优先级的任务,然后需要有一个资源锁来模拟实际场景下对共享资源的获取与释放。一般在 RTOS 中我们会使用互斥锁来对资源进行上锁,因此绝大多数的 RTOS 已经为互斥锁这个组件加入了优先级继承等应对优先级反转问题的功能,包括 RT-Thread(RTT 的互斥锁还具备优先级天花板功能),所以如果我们就是想观察优先级反转的现象,直接使用互斥锁是不行的。

幸运的是,在 RT-Thread 中,还有一个线程间同步组件,其是不带优先级继承等功能的,并且在我们目前的场景中也可以作为资源锁来使用,这就是二值信号量。(特别注意,这边使用二值信号量作为资源锁只是用于观察优先级反转这种异常现象,并且也正是因为其存在这个问题,实际项目中并不推荐这么使用!)

综上,我们的测试代码主体就是三个不同优先级的任务,加上一个资源锁。且我们要测试三种情况,分别是用信号量实现优先级反转,用互斥锁实现优先级继承,在互斥锁的基础上通过设置天花板优先级来实现优先级天花板机制,整体测试代码如下:

#include"rtdef.h"#include"rttypes.h"#include#include#include#include#ifndefRT_USING_NANO#include#endif/* RT_USING_NANO *//* defined the LED0 pin: PB1 */#defineTHREAD_PRIORITY 10#defineUSE_MUTEX 0#defineUSE_PRIORITY_CEILING 0#ifUSE_MUTEXrt_mutex_tmutex;#elsert_sem_tsem;#endifstaticvoidworking(uint32_tnms){ for(volatileuint32_ti =0; i < nms; i++) {        for (volatileuint32_t j = 0; j < 16777; j++) {            __NOP(); // No Operation, just a delay        }    }}void thread_high(void *param) {    /* 让低优先级线程优先获取资源 */    rt_thread_mdelay(2);#if USE_MUTEX    rt_mutex_take(mutex, RT_WAITING_FOREVER);#else    rt_sem_take(sem, RT_WAITING_FOREVER);#endif    working(5); // 模拟高优先级线程对资源的操作#if USE_MUTEX    rt_mutex_release(mutex);#else    rt_sem_release(sem);#endif}void thread_medium(void *param) {    /* 让高优先级线程有机会阻塞 */    rt_thread_mdelay(4);    working(20); // 模拟中优先级线程的工作}void thread_low(void *param) {#if USE_MUTEX    rt_mutex_take(mutex, RT_WAITING_FOREVER);#else    rt_sem_take(sem, RT_WAITING_FOREVER);#endif    working(5); //模拟低优先级线程对资源的操作#if USE_MUTEX    rt_mutex_release(mutex);#else    rt_sem_release(sem);#endif    working(2); // 模拟低优先级线程的其他工作}int main(void) {    rt_thread_t tid = RT_NULL;    rt_thread_mdelay(500);#if USE_MUTEX    mutex = rt_mutex_create("mutex", RT_IPC_FLAG_PRIO);    if (mutex == RT_NULL) {        rt_kprintf("Failed to create mutex\n");        return-1;    }#if USE_PRIORITY_CEILING    // 设置优先级天花板为最高优先级    rt_mutex_setprioceiling(mutex, THREAD_PRIORITY - 2);#endif#else    sem = rt_sem_create("sem", 1, RT_IPC_FLAG_PRIO);    if (sem == RT_NULL) {        rt_kprintf("Failed to create semaphore\n");        return-1;    }#endif    tid = rt_thread_create("th", thread_high, RT_NULL, 2048, THREAD_PRIORITY - 1, 10);    if (tid != RT_NULL) { rt_thread_startup(tid); }    tid = RT_NULL;    tid = rt_thread_create("tm", thread_medium, RT_NULL, 2048, THREAD_PRIORITY, 10);    if (tid != RT_NULL) { rt_thread_startup(tid); }    tid = RT_NULL;    tid = rt_thread_create("tl", thread_low, RT_NULL, 2048, THREAD_PRIORITY + 1, 10);    if (tid != RT_NULL) { rt_thread_startup(tid); }    while (1) { rt_thread_mdelay(100); }}

以上代码动态创建了三个任务:th、tm和tl,分别对应高优先级(9),中优先级(10)和低优先级(11),在 RT-Thread 中优先级数字越小越高。同时还基于条件编译定义了一个资源锁,当USE_MUTEX为0时使用信号量作为锁,用于制造优先级反转,为1且USE_PRIORITY_CEILING为0时使用互斥锁实现优先级继承,USE_MUTEX以及USE_PRIORITY_CEILING都为1时实现优先级天花板策略。

working 函数本质上是制造一段时间的延迟用于模拟任务的工作流程,此处没有使用 rt_thread_mdelay 延时的原因是 rt_thread_mdelay 会造成任务本身主动让出,从而无法实现真实情况下高优先级在低优先级任务运行中的抢占过程。

th 任务首先使用 rt_thread_mdelay 延迟主动让出使得低优先级任务能够先运行从而获取到资源,紧接着尝试获取资源,获取到后使用 working 函数模拟对资源进行的操作,最后释放资源。

tm 任务首先使用 rt_thread_mdelay 延迟主动让出使得高优先级任务能够有时间运行到尝试获取资源并阻塞,从而能够成功制造出中优先级对低优先级任务的抢占。紧接着使用working函数模拟其运行。注意,该任务中没有任何对资源的操作,也不会获取与释放锁。

tl 任务开始运行后立马尝试获取资源,进而使用working函数模拟对资源进行的操作,接着释放资源,最后再次使用working函数模拟该任务的其他操作。

在开启了优先级天花板机制的情况下,互斥锁创建后会使用 rt_mutex_setprioceiling 为其设置一个天花板优先级,在这里也就是 THREAD_PRIORITY - 2,虽然我们所有任务的最高优先级为 THREAD_PRIORITY - 1,由于 RT-Thread 存在时间片轮转调度功能,同优先级任务在时间片用完之后也能互相抢占。此处也可以设置一个比较大的时间片来临时防止抢占,我这里就直接将该优先级设置为更高一级,从根本上避免抢占的发生。

OK,所有功能点都讲解完成,接下来我们就借助 RT-Trace 的运行跟踪功能,进入 CPU 视角,来看看底层的真实运行情况。

首先是使用信号量实现的优先级反转场景,我们重点关注三个任务之间的运行,屏蔽掉中断,空闲线程等其他信息:

e70a91ce-7b0e-11f0-9080-92fbcf53809c.png

可以看到 tl 在开始运行了一段时间后 th 发生了抢占并尝试获取资源,但由于当前资源被 tl 持有,于是 th 立马阻塞让出了 CPU 等待 tl 释放资源:

然而还没等 tl 释放资源,中等优先级的 tm 就抢占了 tl 的运行,此时 th 虽有着最高优先级,但也只能眼睁睁等着 tm 执行完,因为要等 tl 释放资源它才能运行,但 tl 优先级没有 tm 高,当前情况下 tm 不运行完, tl 根本不会运行:

e724c404-7b0e-11f0-9080-92fbcf53809c.png

等 tm 终于运行完了,把 CPU 给到 tl,并且 tl 释放了资源后,本应是最高优先级的 th 才得以运行:

由于低优先级任务持有了高优先级任务运行所需的共享资源,从而导致高优先级任务不可避免地进入等待,而当系统比较复杂,任务较多时,低优先级任务又特别容易被其他中优先级任务抢占,最终导致高优先级任务被无限延后,直至所有抢占的任务运行完并且低优先级任务释放资源才得以运行,而这个过程需要等待多久是未知且不可控的。在很多工业、医疗、军事领域中,一个任务之所以要定义为高优先级,就是需要其尽快执行,并且运行周期严格可控,否则很有可能造成不可挽回的后果,如果这个过程中遇到优先级反转,那系统异常甚至崩溃几乎是必然的!

现在我们将 USE_MUTEX 置 1 来使用带有优先级继承功能的互斥锁,此时的任务调度过程如下:

e73ddad4-7b0e-11f0-9080-92fbcf53809c.png

很明显,此时中优先级任务没有能抢占低优先级任务在共享资源处理阶段的运行,虽然高优先级任务第一次尝试获取资源会失败,但这个过程也临时抬升了tl的优先级:

e74aeff8-7b0e-11f0-9080-92fbcf53809c.png

而后 tl 得以不被中优先级的 tm 打断,一路运行到释放资源,紧接着 th 获取资源执行,而 tm 自然是等到 th 运行完后才能运行。很明显,优先级反转带来的影响在这种机制下得到大幅缓解,但这里仍然有一个小瑕疵:

e75360c0-7b0e-11f0-9080-92fbcf53809c.png

在 tl 对共享资源进行处理的过程中,仍然发生了一次抢占,这期间还会动态修改任务的优先级,稍许拉长了tl 对资源的处理时间。如果频繁操作资源,那么这对系统调度来说也是消耗比较大的。如果想要保证高优先级任务能够尽快执行,那么这一段操作一定是要去除的。如何去除?这就引出了下面这个机制。

我们再将 USE_PRIORITY_CEILING 置 1 打开优先级天花板,注意优先级天花板的值是预设且静态的,所以我们在使用这个特性的时候需要提前设置好值:

rt_mutex_setprioceiling(mutex, THREAD_PRIORITY -2);

这个值一般会设置成使用资源中所有任务的最高优先级,当然文章开头也讲过由于 RT-Thread 相同优先级可以通过轮转调度机制相互抢占,我这边为了避免这种情况,所以设置为最高任务优先级的更高一级。

我们来看下加入了优先级天花板后任务的调度情况:

e75f0e34-7b0e-11f0-9080-92fbcf53809c.png

乍一看好像与优先级继承的调度情况没有区别,接下来我将 tl 操作共享资源的时间轴放大:

e7683716-7b0e-11f0-9080-92fbcf53809c.png

对比优先级继承机制下的这部分:

e7758268-7b0e-11f0-9080-92fbcf53809c.png

可以看到中间 th 对 tl 的抢占过程消失了!

e77eafa0-7b0e-11f0-9080-92fbcf53809c.png

正是由于我们设置了天花板优先级,tl 在获取资源的那一刻就将其优先级提升到了天花板,因此在这过程中即使是高优先级的 th 也无法对其进行抢占,真正做到了最快的资源处理速度,自然 th 在这种情况下也得到了最快的响应运行速度。

通过上述内容,相信大家已经对优先级反转,优先级继承以及优先级天花板有了非常直观的理解,在实际的项目中也能够按照实际需求去选择合适的机制。

最后提一点,以上两种方式都是缓解优先级反转带来的影响,并不能解决,因为低优先级任务获取到资源后高优先级任务不可避免地要等待,否则就会造成更为致命的数据同步问题,上述解决方案都只是尽可能让低优先级任务更快速地处理完释放资源从而让高优先级任务及时获取。如果你的系统就不能接受这种情况,那么或许你要从程序设计角度,根本上去避免低优先级任务与高优先级任务共享资源,或是通过精密的设计规避高优先级任务想要获取资源时低优先级任务正在处理资源的情况。