0 前言
什么是临界区(临界段)?
裸机编程中由于不涉及线程和线程切换,因此没有临界区这一个概念。在RTOS中由于存在线程切换等场景,便有了临界区这个概念。简单来说,临界区就是不允许被中断的代码区域。什么时候代码执行的过程会被打断:一个是线程切换,一个是异常事件。对于RT-Thread而言实际上都是异常事件导致代码执行过程被中断,因为RT-Thread是在PendSV事件内进行线程切换的。
什么样的代码区域应该作为临界区保护起来?
假如我们有2个线程A和B,它们都有可能使用同一个串口向外打印数据,而我们希望线程A和B在执行printf这个操作时是不会被打断的,否则我们看到的串口打印数据将杂乱无章。下面是一个例子:
void thread_a(void *arg)
{
for (;;)
{
printf("A thread!\r\n");
}
}
void thread_b(void *arg)
{
for (;;)
{
printf("B thread!\r\n");
}
}
假设线程A和线程B是按照时间片轮转进行调度的,那么就有可能出现线程A正在打印"A thread!\r\n"时被线程B获得CPU使用权进而打印线程B的"B thread!\r\n",最后呈现在串口上的数据杂乱无章。这不是我们希望看到的结果,因此我们需要将printf作为临界区保护起来,以RT-Thread临界区保护API为例,修改后的代码如下:
void thread_a(void *arg)
{
for (;;)
{
rt_enter_critical(); // 进入临界区
printf("A thread!\r\n");
rt_exit_critical(); // 退出临界区
}
}
void thread_b(void *arg)
{
for (;;)
{
rt_enter_critical(); // 进入临界区
printf("B thread!\r\n");
rt_exit_critical(); // 退出临界区
}
}
1 RTOS中临界区保护的实现原理(基于RT-Thread)
前面我们已经知道导致代码段被中断的原因就是发生了异常事件,为了实现对临界区的保护可以选择最简单直接的方法:关闭全部中断(NMI fault和硬fault除外),为了避免直接关闭中断带来的不良影响,RT-Thread为我们提供了2种方案:
(1)开关全部中断
(2)使能/失能线程调度
1.1 开关全部中断
ARM的M3/M4/M7内核为了快速开关中断,专门设置了一条CPS指令,共有以下4种用法:
CPSID I ;PRIMASK=1, ;关中断
CPSIE I ;PRIMASK=0, ;开中断
CPSID F ;FAULTMASK=1, ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
有关上述寄存器的定义如下:
这里我们并不想关闭硬件fault,因此只会使用CPSID I和CPSIE I这2条指令。对于没有出现临界区嵌套的情况,直接在进入临界区前关中断,退出临界区后关中断即可实现对临界区的保护。但如果临界区出现嵌套的话,只是单纯使用开关中断显然无法满足我们要求,看看RT-Thread如何解决这个问题的:
// 关全部中断
rt_base_t rt_hw_interrupt_disable(void);
;/*
; * rt_base_t rt_hw_interrupt_disable();
; */
rt_hw_interrupt_disable PROC
EXPORT rt_hw_interrupt_disable
MRS r0, PRIMASK
CPSID I
BX LR
ENDP
// 开全部中断
void rt_hw_interrupt_enable(rt_base_t level);
;/*
; * void rt_hw_interrupt_enable(rt_base_t level);
; */
rt_hw_interrupt_enable PROC
EXPORT rt_hw_interrupt_enable
MSR PRIMASK, r0
BX LR
ENDP
可以看到,我们使能中断时是需要传入一个rt_base_t类型参数,这个就是RT-Thread实现临界区嵌套保护的关键。我们使用时只需要按照如下格式进行嵌套保护即可,下面是一个一重临界区嵌套保护的示例:
rt_base_t level1;
rt_base_t level2;
void thread_a(void *arg)
{
for (;;)
{
level1 = rt_hw_interrupt_disable();
printf("A thread!\r\n");
level2 = rt_hw_interrupt_disable();
printf("A thread!!\r\n");
rt_hw_interrupt_enable(level2);
printf("A thread!!!\r\n");
rt_hw_interrupt_enable(level1);
}
}
结合前面的汇编代码,我们很容易可以发现,只有在执行最后一个rt_hw_interrupt_enable操作时才会打开全部中断,真正实现嵌套临界区的保护。
它的实现原理就是使用全局变量保存进入临界区之前的全部中断开关状态,然后退出临界区时根据这个开关状态来进行全部中断开关。根据临界区嵌套保护语句的顺序,显然只有执行了最后一个rt_hw_interrupt_enable才会打开全部中断。
1.2 使能/失能线程调度
相比前面直接开关中断来实现临界区保护,使能/失能线程调度则显得“温柔”很多,它并不会长时间关闭中断对我们的一些中断事件(例如SysTick时间基准)造成影响,如果我们的线程并不会被中断服务函数内的操作影响,推荐使用使能/失能线程调度的方法去实现临界段保护,RT-Thread提供的相关函数如下:
// 失能线程调度
/**
* This function will lock the thread scheduler.
*/
void rt_enter_critical(void)
{
register rt_base_t level;
/* disable interrupt */
level = rt_hw_interrupt_disable();
/*
* the maximal number of nest is RT_UINT16_MAX, which is big
* enough and does not check here
*/
rt_scheduler_lock_nest ++;
/* enable interrupt */
rt_hw_interrupt_enable(level);
}
;/*
; * rt_base_t rt_hw_interrupt_disable();
; */
rt_hw_interrupt_disable PROC
EXPORT rt_hw_interrupt_disable
MRS r0, PRIMASK
CPSID I
BX LR
ENDP
// 使能线程调度
/**
* This function will unlock the thread scheduler.
*/
void rt_exit_critical(void)
{
register rt_base_t level;
/* disable interrupt */
level = rt_hw_interrupt_disable();
rt_scheduler_lock_nest --;
if (rt_scheduler_lock_nest <= 0)
{
rt_scheduler_lock_nest = 0;
/* enable interrupt */
rt_hw_interrupt_enable(level);
if (rt_current_thread)
{
/* if scheduler is started, do a schedule */
rt_schedule();
}
}
else
{
/* enable interrupt */
rt_hw_interrupt_enable(level);
}
}
;/*
; * void rt_hw_interrupt_enable(rt_base_t level);
; */
rt_hw_interrupt_enable PROC
EXPORT rt_hw_interrupt_enable
MSR PRIMASK, r0
BX LR
ENDP
(1)rt_enter_critical函数
首先关闭全部中断,将rt_scheduler_lock_nest自增1,然后将中断状态还原到rt_enter_critical函数最开始的状态。
(2)rt_exit_critical函数
首先关闭全部中断,将rt_scheduler_lock_nest自减1,如果rt_scheduler_lock_nest<=0表示临界区保护结束需要退出临界区,这时rt_scheduler_lock_nest=0然后将中断状态还原到函数最开始的状态,如果调度已经开始则发起一次调度尽可能保证更高优先级线程能够得到及时运行。如果rt_scheduler_lock_nest>0表示临界区保护还未结束,将中断状态还原到函数最开始的状态。
实现临界区嵌套保护的核心就是rt_scheduler_lock_nest,借助嵌套临界区保护操作成对出现这一特点,使用rt_scheduler_lock_nest调度锁表征当前临界区保护嵌套执行情况,当rt_scheduler_lock_nest<=0时则可以退出临界区保护,因此rt_exit_critical()和rt_exit_critical()无需人为定义全局变量就可以实现临界区嵌套保护,使用起来更加方便。