基于STM32的mbedOS优先级反转问题机制剖析
2023-02-17叶柯阳王宜怀徐婷婷刘长勇
叶柯阳 王宜怀 徐婷婷 刘长勇
1(苏州大学计算机科学与技术学院 江苏 苏州 215006) 2(武夷学院数学与计算机学院 福建 武夷山 354300)
0 引 言
mbedOS是一款面向ARM Cortex-M系列处理器的开源实时操作系统(Real-Time Operating System,RTOS)[1]。它于2014年由ARM公司推出,专为物联网 (Internet of Things,IoT) 中的“物体”设计[2],包含了开发基于Arm Cortex-M微控制器的联网产品所需的所有特性,包括安全性、连接性、RTOS传感器和I/O设备的驱动程序,具有堆栈共用和事件驱动两大代表性特点。
目前,针对嵌入式实时操作系统优先级反转问题的研究集中在VxWorks操作系统、WinCE操作系统、μC/OS-Ⅱ操作系统等,而对mbedOS实时操作系统避免优先级反转问题的机制研究方面缺乏相关材料。为此,本文将利用Cortex-M4内核的STM32微控制器,基于Kinetis Design Studio 3.1.0 IDE开发环境和SD-mbedOS工程框架[3],通过一个测试工程对mbedOS避免优先级反转问题的机制进行分析,剖析其从各线程启动,到申请互斥量,再到最终释放互斥量的全过程,结合相关函数、关键代码、流程图、时序图等分析其实现的原理。通过对mbedOS避免优先级反转问题机制的剖析,可为 mbedOS的应用研究和在不同微控制器上的移植提供基础,也可为不同实时操作系统下避免优先级反转问题机制的比较分析提供参考。
1 优先级反转问题描述
1.1 历史问题
“火星探路者”于1997年7月4日在火星表面着陆。在开始的几天内工作稳定,并传回大量数据,但是几天后,“探路者”开始出现系统复位、数据丢失的现象。究其原因是发生了优先级反转问题[4]。
1.2 一般性描述
当线程以独占方式使用共享资源时,可能出现低优先级线程先于高优先级线程被运行的现象,这就是优先级反转问题,可进行如下一般性描述。
假设有三个线程Ta、Tb、Tc,其优先级分别记为Pa、Pb、Pc,且有Pa>Pb>Pc,Ta和Tc使用一个共享资源S,Tb并不使用共享资源S,用信号量x(x=0,1)标识对S的独占访问,初始时x=1,表示可独占S。设t0时刻,Tc先运行并获取信号量(即x由1变为0,表示S已被占用),使用S。t1时刻,Ta被调度运行,因为Pa>Pc,故抢占Tc获得CPU使用权。Ta运行至t2时刻,需访问S,但Tc并没有释放S(即x仍为0),所以Ta放入阻塞队列,直到x=1,才能从阻塞队列中移出,放入就绪队列,被重新调度运行。t3时刻,Tb抢占Tc获得运行,此时就出现了Tb虽然优先级比Ta低,却比Ta先运行的现象,不合理,这就是优先级反转问题。表1给出了上述过程的运行时序。
表1 优先级反转过程
1.3 避免优先级反转问题的意义
对于嵌入式实时操作系统来说,最重要的指标在于确认线程执行的时间是可预测的,即要确保在任何时刻执行某个线程都不能超过某个特定的时间。然而由于本身基于优先级设计的线程,每个优先级不同的线程往往对应着现实中执行的任务,若发生了优先级反转,会导致低优先级线程比高优先级线程先执行,造成线程调度时时间的不确定性,破坏实时系统的实时性,严重时可能导致系统崩溃。因此处理好这类问题对于实时操作系统的正常运行至关重要。
2 优先级反转问题避免方法
2.1 基本方法
常见的避免优先级反转方法一般有两种,分别为优先级继承和优先级天花板。
(1) 优先级继承。优先级继承(priority inheritance)是指通过临时提升持有资源的低优先级线程的优先级至请求访问同一资源的高优先级线程的优先级来避免优先级反转的方法[5]。
(2)优先级天花板。优先级天花板(priority ceiling)是指通过将申请并得到资源的线程的优先级临时提升至所有可能使用该资源的线程中最高优先级线程的优先级(即天花板)来避免优先级反转的方法[5]。
2.2 操作系统层面解决方法
许多嵌入式实时操作系统,例如μC/OS-Ⅱ、VxWorks、WinCE等针对这一问题都有自己的处理方式。μC/OS-Ⅱ操作系统提供互斥信号量mutex来避免优先级反转,其利用的是优先级置顶协议[6]。VxWorks操作系统对优先级反转问题采用优先级继承协议,整体上使用互斥信号量,按优先级与先入先出队列两种方式排列等待对该互斥信号量进行上锁的线程,在创建互斥信号量时,选择SEM_INVERSION_ SAFE与SEM_Q_PRIORITY两种选择值的域,即可避免优先权反转[7]。WinCE实时操作系统软件采用优先级继承方式来避免优先级反转,当高优先级线程被低优先级线程阻塞时,会提升低优先级线程的优先级至高优先级线程的优先级,从而避免优先级反转现象[8]。
3 mbedOS优先级反转问题机制流程分析
本文将使用一个测试工程来对mbedOS避免优先级反转问题机制进行流程分析。首先对部分关键代码进行分析,然后给出调度时序,最后对执行流程进行分段解析。测试工程的功能是:创建三个用户线程Ta、Tb、Tc,初始优先级分别设置为26、25、24(在mbedOS中数字越大优先级越高,将此处三个线程的优先级分别命名为Pa、Pb、Pc),启动运行顺序为Tc、Ta、Tb,其中Tc和Ta使用同一互斥量来申请对共享资源S的独占使用,Tb并不使用共享资源S。测试工程通过串口输出三个线程调度时使用互斥量避免优先级反转的详细过程。为了确保整体执行流程能够循环执行,过程中使用到了延时函数(会将线程放入延时队列),此处模拟线程到达的先后顺序为:经过1秒后Tc到达,再经过4秒后Ta、Tb到达(注意此处到达顺序为先Ta后Tb)。测试工程的执行流程如图1所示。
图1 测试工程执行流程
3.1 关键代码分析
3.1.1互斥量控制块
在mbedOS中使用互斥量控制块的方式来描述互斥量,数据结构如下:
typedef struct osRtxMutex_s
{
uint8_t id;
//互斥量ID
uint8_t state;
//互斥量状态
uint8_t flags;
//互斥量标志
uint8_t attr;
//互斥量属性
const char *name;
//互斥量名称
osRtxThread_t *thread_list;
//互斥量阻塞队列
osRtxThread_t *owner_thread;
//互斥量私有线程
struct osRtxMutex_s *owner_prev;
//指向前一个互斥量
struct osRtxMutex_s *owner_next;
//指向下一个互斥量
uint8_t lock;
//互斥锁
uint8_t padding[3];
//保留
} osRtxMutex_t;
其中互斥量属性attr包括嵌套型互斥量(osMutexRecursive)、内部优先级互斥量(osMutexPrioInherit)和健壮互斥量(osMutexRobust)。当高优先级的线程等待已被低优先级的线程锁定互斥量时,若该互斥量拥有内部优先级属性,则低优先级的线程会以高优先级线程的优先级运行,这种方式是以继承的形式进行传递的。当线程解锁互斥量时,线程的优先级自动变为它原来的优先级。mbedOS正是利用了这一关键属性来避免优先级反转问题。
3.1.2互斥量锁定函数
在mbedOS中,线程可通过使用互斥量对象调用lock函数的方式申请锁定互斥量。lock函数内部调用顺序为lock→osMutexAcquire→_ _svcMutexAcquire→触发SVC中断服务程序SVC_Handler→实际互斥量锁定函数svcRtxMutexAcquire。
其中涉及到优先级部分的关键代码如下:
if ((mutex->attr & osMutexPrioInherit) !=0U) {
if (mutex->owner_thread->priority
mutex->owner_thread->priority=thread->priority;
osRtxThreadListSort(mutex->owner_thread);
}
}
代码段分析:if((mutex->attr & osMutexPrioInherit)!=0U)语句表示判断该互斥量对象是否具有内部优先级属性,若包含则进一步判断该互斥量私有线程的优先级mutex->owner_thread->priority是否小于当前运行线程的优先级thread->priority,若小于则将当前运行线程的优先级赋值给该互斥量私有线程的优先级,即所谓的优先级继承。
3.1.3互斥量解锁函数
在mbedOS中,线程可通过使用互斥量对象调用unlock函数的方式申请解锁互斥量。unlock函数内部调用顺序为unlock→osMutexRelease→_ _svcMutexRelease→触发SVC中断服务程序SVC_Handler→实际互斥量解锁函数svcRtxMutexRelease。
其中涉及到优先级部分的关键代码如下:
if ((mutex->attr & osMutexPrioInherit)!=0U) {
priority=thread->priority_base;
mutex0=thread->mutex_list;
while (mutex0 != NULL) {
if ((mutex0->thread_list!=NULL) && (mutex0->thread_list->priority>priority)) {
priority=mutex0->thread_list->priority;
}
mutex0=mutex0->owner_next;
}
thread->priority=priority;
}
代码段分析:if((mutex->attr & osMutexPrioInherit)!=0U)语句表示判断该互斥量是否具有内部优先级属性,priority和mutex0均表示临时局部变量,thread表示当前运行线程。若该互斥量包含内部优先级属性,则首先获取当前运行线程初始化时的优先级thread->priority_base赋值给priority以及当前运行线程所拥有的互斥量列表指针thread->mutex_list赋值给mutex0。然后使用while循环找到mutex0所在的互斥量列表中的所有互斥量所拥有的所有线程,获取优先级最高的线程的优先级并赋值给priority。最后将当前运行线程的优先级变为priority。
特别要指出的是,上述while循环语句是为了避免当低优先级线程与多个高优先级线程嵌套使用多个互斥量时可能造成的优先级反转现象。由于只有当系统中存在三个或三个以上线程时才可能发生优先级反转现象,故此处以四个线程为例。假设有两个高优先级线程Ta和Tb、一个中优先级线程Tc以及一个低优先级线程Td,优先级分别为Pa、Pb、Pc、Pd,其中Pa>Pb>Pc>Pd,Ta和Td使用同一互斥量mutex1共享资源S1, Tb和Td使用同一互斥量mutex2共享资源S2,且Td首先申请锁定互斥量mutex2,再在释放mutex2之前申请锁定互斥量mutex1(连续锁定多个互斥量遵循后进先出原则,即后锁定的互斥量先释放)。设t0时刻,Td首先到达并锁定了互斥量mutex2和互斥量mutex1。t1时刻Ta和Tb到达,等待Td解锁各自需要的互斥量。Tc也在t1时刻到达,由于Ta和Tb调用互斥量锁定函数svcRtxMutexAcquire时已将Td的优先级提升至最高值Pa,故Tc会进入等待状态,等待Td运行。t2时刻,Td解锁互斥量mutex1,若此时未使用上述while循环语句,Td会直接降为其初始优先级Pd,此时Ta会抢占Td获得CPU使用权,锁定互斥量mutex1并执行。t3时刻,Ta运行完毕,此时处于等待状态的Td由于优先级低于Tc,会被Tc抢占CPU使用权,使得Td无法解锁互斥量mutex2,从而导致优先级低于Tb的线程Tc却先于Tb运行,造成优先级反转现象,具体过程如表2所示。
表2 四线程优先级反转过程
续表2
若只涉及到一个低优先级线程和一个高优先级线程对于同一个互斥量的使用,则在低优先级线程(即上述代码中的thread)解锁互斥量的过程中,由于thread拥有的互斥量列表已在执行上述代码之前释放,故mutex0为空,while循环实际上并不执行,而是直接通过语句:
priority=thread->priority_base;
thread->priority=priority;
将thread的优先级转变为其初始优先级。
3.2 优先级反转问题测试工程执行流程分析
本文的测试工程在STM32微控制器[13]上进行。STM32片内Flash存储区大小为256 KB,一般用来存放中断向量、程序代码、常量等;片内RAM存储区大小为32 KB,一般用于存放初始化的全局变量、静态变量、局部变量等。
3.2.1线程调度时序分析
测试工程涉及到的三个线程具体调度时序图如图2所示。
图2 线程调度时序图
其中“▯”表示线程或队列的有效运行时间,实线箭头表示线程运行、进入队列、申请互斥量或改变优先级,虚线箭头表示从队列取线程(互斥量)或返回申请互斥量结果。
3.2.2执行流程分段解析
目前,国内外针对嵌入式实时操作系统的优先级反转问题采用的分析方法,大多为一个基于示例图的浅层实验[9-10],均没有完整地将处理机制分析清楚。针对上述问题,本文将采用基于时序图并在代码中相应位置插入printf语句的方法进行分析。插入printf语句的分析方式是应用最广泛的调试技术之一,可应用于计算机视觉中来输出中间结果[11],亦可应用于程序功能分析中得到故障产生的信息[12]。该方法具有简单、清晰、直观等优点。
(1) 线程启动。从芯片上电到mbedOS 启动完成后,会最终转到app_init函数执行,然后可在app_init函数中创建用户线程[3]。在该函数中创建并先后启动了三个用户线程Tc、Ta、Tb,然后阻塞该函数的运行。为确保线程能正常被创建,不被其他线程打断,在创建用户线程的过程中,使用了互斥量。
printf输出结果如下:
0-1.当前运行的主线程(2000FF80)启动线程Tc.
4-1.互斥量(20001694)的互斥锁=0,表示未锁定,当前运行线程(2000FF80)可以申请该互斥量
4-2.互斥锁变为1,表示互斥量申请成功
8.互斥锁变为0,表示完全解锁,将当前互斥量(20001694)从当前运行线程(2000FF80)拥有的互斥量列表(2000FFAC)中移除
*9-1.当前线程=2000FF80的初始优先级=24,当前优先级=24
*9-2.释放互斥量后,当前线程=2000FF80的初始优先级=24,当前优先级=24
0-2.当前运行的主线程(2000FF80)启动线程Ta.
4-1.互斥量(20001814)的互斥锁=0,表示未锁定,当前运行线程(2000FF80)可以申请该互斥量
4-2.互斥锁变为1,表示互斥量申请成功
8.互斥锁变为0,表示完全解锁,将当前互斥量(20001814)从当前运行线程(2000FF80)拥有的互斥量列表(2000FFAC)中移除
*9-1.当前线程=2000FF80的初始优先级=24,当前优先级=24
*9-2.释放互斥量后,当前线程=2000FF80的初始优先级=24,当前优先级=24
0-3.当前运行的主线程(2000FF80)启动线程Tb.
4-1.互斥量(20001754)的互斥锁=0,表示未锁定,当前运行线程(2000FF80)可以申请该互斥量
4-2.互斥锁变为1,表示互斥量申请成功
8.互斥锁变为0,表示完全解锁,将当前互斥量(20001754)从当前运行线程(2000FF80)拥有的互斥量列表(2000FFAC)中移除
*9-1.当前线程=2000FF80的初始优先级=24,当前优先级=24
*9-2.释放互斥量后,当前线程=2000FF80的初始优先级=24,当前优先级=24
******Tc、Ta和Tb启动完成,同时阻塞主线程******
(2) Tc申请锁定互斥量。阻塞主线程后,由于初始时Tc延时1秒,而Ta和Tb延时5秒,故Tc会先从延时队列中移出放入就绪队列,抢占空闲线程获得CPU使用权。由于互斥锁为0,Tc申请锁定互斥量成功,互斥锁变为1,同时点亮蓝灯。
printf输出结果如下:
5.当前就绪队列中最高优先级线程(20001834)取代当前运行线程(200016B4),开始运行
1.Tc(200016B4)获得CPU使用权,蓝灯亮
1-1.Tc申请锁定互斥量
4-1.互斥量(20001880)的互斥锁=0,表示未锁定,当前运行线程(200016B4)可以申请该互斥量
4-2.互斥锁变为1,表示互斥量申请成功
1-2.Tc锁定互斥量成功,将锁定15秒
(3) Ta申请锁定互斥量。在Tc锁定互斥量4秒后,Ta和Tb从延时队列中移出,放入到就绪队列,由于Pa大于Pb,故mbedOS会从就绪队列中取出Ta激活运行。又因为Pa大于Pc,故Ta会抢占Tc获得CPU使用权,Tc被放入就绪队列,同时熄灭蓝灯。
但当Ta运行至申请锁定互斥量时,由于此时互斥量已被Tc锁定(互斥锁为1),Ta申请互斥量失败,因此会将Tc的优先级提升至与Ta的优先级相同(即使用优先级继承方法将Tc的优先级提升至Pa),然后将Tc放入就绪队列重新排序,Ta自身进入等待队列和互斥量阻塞队列,将CPU使用权让给Tc,等待Tc解锁互斥量。
printf输出结果如下:
5.当前就绪队列中最高优先级线程(20001834)取代当前运行线程(200016B4),开始运行
2.Ta(20001834)抢占Tc获得CPU使用权,蓝灯暗
2-1.Ta申请锁定互斥量
6.互斥锁=1,表示已锁定(其所有者线程=200016B4),互斥量申请失败
*7-1.优先级继承前,当前互斥量私有线程=200016B4的优先级=24低于当前运行线程=20001834的优先级=26
*7-2.优先级继承后,当前互斥量私有线程=200016B4的优先级被提升至与当前运行线程=20001834的优先级=26相同
*7-3.将当前互斥量私有线程=200016B4从互斥量私有线程列表中移出,并放到就绪队列(200001D0)中重新排序
6-1.将当前运行线程(20001834)放到等待队列(200001E4)
6-2.从就绪队列(200001D0)获取优先级最高的线程(200016B4),并设置为激活态准备运行
6-3.将当前运行线程(20001834)放入互斥量阻塞队列(20001888):20001834->0->80019A9
(4) Tc解锁互斥量。Tc重新获得CPU使用权后,继续运行。由于互斥量是由Tc锁定的,因此Tc能成功解锁互斥量。在解锁过程中,由于当前运行线程Tc将互斥量从它的互斥量列表中移出,故此时mutex0为空,因此在互斥量解锁函数svcRtxReleaseMutex中会直接将Tc的优先级降为其初始优先级Pc。解锁后互斥锁为0,同时点亮蓝灯,Tc放入就绪队列,又开始等待执行新一轮的执行过程。此时互斥量会从互斥量列表移出,并移转给正在等待互斥量的Ta,之后Ta放入就绪队列,由于Pa>Pb>Pc,故在就绪队列中Ta处于队首位置。mbedOS从就绪队列中取出优先级最高的Ta激活运行,Ta成功锁定互斥量,互斥锁变为1。
printf输出结果如下:
1-3.Tc解锁互斥量成功,蓝灯亮
8.互斥锁变为0,表示完全解锁,将当前互斥量(20001880)从当前运行线程(200016B4)拥有的互斥量列表(200016E0)中移除
*9-1.当前线程=200016B4的初始优先级=24,当前优先级=26
*9-2.释放互斥量后,当前线程=200016B4的初始优先级=24,当前优先级=24
10-1.从互斥量阻塞队列(20001888)中获取优先级最高的互斥量等待线程=20001834
10-2.将线程(20001834)从等待队列(200001E4)中移出
10-3.将线程(20001834)放到就绪队列(200001D0)
11.此时就绪队列=200001D0中的线程:20001834->20001774->20001328
10-4.将刚获取的线程(20001834)设置为互斥量所有者,互斥锁变为1
(5) Ta解锁互斥量。Ta运行5秒后,由于互斥量是由Ta锁定的,因此Ta能成功解锁互斥量,解锁后互斥锁为0,同时熄灭蓝灯。互斥量从互斥量列表移出,同时为了重复上述演示过程,Ta放入延时队列5秒,5秒之后从延时队列移出放入就绪队列,又开始等待执行新一轮的执行过程。
printf输出结果如下:
5.当前就绪队列中最高优先级线程(20001834)取代当前运行线程(200016B4),开始运行
2-2.Ta锁定互斥量成功,将锁定5秒
2-3.Ta解锁互斥量成功,蓝灯暗
8.互斥锁变为0,表示完全解锁,将当前互斥量(20001880)从当前运行线程(20001834)拥有的互斥量列表(20001860)中移除
*9-1.当前线程=20001834的初始优先级=26,当前优先级=26
*9-2.释放互斥量后,当前线程=20001834的初始优先级=26,当前优先级=26
(6) Tb运行。在Ta进入延时队列后,mbedOS从就绪队列中取出优先级最高的Tb激活运行。Tb运行5秒后释放CPU使用权,为了重复上述演示过程,Tb放入延时队列4秒,之后从延时队中移出放入就绪队列,又开始等待执行新一轮的执行过程。printf输出结果如下:
3.Tb(20001774)获得CPU使用权,将运行5秒,成功避免优先级反转
5.当前就绪队列中最高优先级线程(20001834)取代当前运行线程(20001774),开始运行
3-1.taskB释放CPU使用权
其中地址2000FF80表示主线程,地址200016B4表示Tc,地址20001834表示Ta,地址20001774表示Tb,地址80019A9表示缺省处理函数DefaultISR,地址20001328表示空闲线程(系统中无运行线程时该线程被调度运行)。
4 mbedOS优先级反转问题机制归纳
通过上述流程分析,mbedOS中避免优先级反转问题机制可归纳如下:
mbedOS整体上使用的是基于互斥量的优先级继承方法来避免优先级反转,其中优先级继承方法包括优先级提升与优先级恢复两部分。当线程调用svcRtxMutexAcquire函数申请锁定互斥量时,系统首先判断该互斥量是否已锁定,若已锁定,则进一步判断该互斥量是否具有内部优先级属性osMutexPrioInherit,若是,则通过语句mutex->owner_thread->priority=thread->priority将当前运行线程(thread)的优先级赋给该互斥量的私有线程(mutex->owner_thread),即优先级继承方法中的优先级提升;当线程调用svcRtxMutexRelease函数申请锁定互斥量时,系统首先判断该互斥量是否具有内部优先级属性osMutexPrioInherit,若是,则获取当前运行线程的互斥量列表,若除了该互斥量以外,还具有其他互斥量,则遍历其他互斥量的私有线程列表,获取其中最高优先级线程的优先级,并将其赋给当前运行线程,否则直接将当前运行线程优先级变为其初始优先级,即优先级继承方法中的优先级恢复。
5 结 语
优先级反转问题是每一个实时操作系统所必须要考虑到的问题,一旦出现,就会破坏系统的实时性,轻则造成系统逻辑紊乱,严重时甚至会导致系统崩溃。本文首先通过历史上出现过的问题引出优先级反转,并分析其现象、避免的意义以及基本解决方案,然后利用Cortex-M4内核的STM32微控制器,基于Kinetis Design Studio 3.1.0 IDE开发环境和SD-mbedOS工程框架,构建一个测试工程对mbedOS避免优先级反转问题的机制进行了详细剖析。通过剖析,有助于快速理解mbedOS中互斥量的使用以及避免优先级反转问题的详细过程,可为 mbedOS的应用研究和在不同微控制器上的移植提供基础,也可为不同实时操作系统下避免优先级反转问题机制的比较分析提供参考。