RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
1 中断锁
- 定义
中断锁即为全局中断开关,是禁止多线程访问临界区最简单的一种方式,即通过关闭中断的方式,来保证当前线程不会被其他事件打断(此时系统不再响应可以触发线程调度的外部事件),除非这个线程主动放弃了处理器控制权。
CM3中线程调度是利用PendSV悬起异常完成的,PendSV中断的优先级一般设为最低(不能让线程抢占中断获取CPU的使用权),因此关闭中断后,系统不再响应可触发线程调度的外部事件。因此,如果在中断锁保护的临界区内主动发起线程调度,需要等中断锁打开后才能执行。
注意:这里的关中断,只是屏蔽了全局中断请求,配置使能的中断在触发后,不会立即执行中断服务例程ISR,等中断打开后才会执行。
- 应用场合与注意事项
中断锁是最强大的和最高效的同步方法,使用中断锁来操作临界区的方法可以应用于任何场合,其他几类同步方式(如信号量、互斥量等)均依赖于中断锁实现。但中断锁对系统实时性影响巨大,关闭中断会导致整个系统不能响应外部中断,使用时需保证关闭中断的时间非常短,遵循“快进快出”原则,否则会导致系统完全无实时性可言。
例如:某时刻有一个线程运行时进入临界段,并且采用中断锁保护,此时若有一个紧急的中断事件被触发,该中断就会被挂起,不能及时响应,必须等待中断开启后才可以得到响应,如果关中断时间超过了紧急中断能容忍的限度,危害可想而知。
RT-Thread 源码中有许多处临界段的地方,临界段虽然用中断锁保护了关键代码的执行不被打断,但也会影响系统的实时,任何使用了操作系统的中断响应都不会比裸机快。
在中断服务函数中也可以使用中断锁,防止被更高优先级的中断打断(CM3支持中断嵌套)
1.1 中断锁函数接口
- 关全局中断
rt_base_t rt_hw_interrupt_disable(void);
具体通过汇编指令实现,本人使用的是GCC环境,汇编代码(在context_gcc.S
中)如下:
.global rt_hw_interrupt_disable ; 全局声明,表示可以从其他文件访问
.type rt_hw_interrupt_disable, %function ; 伪指令定义函数
rt_hw_interrupt_disable:
MRS R0, PRIMASK ; 读取 PRIMASK 寄存器的值到 r0 寄存器
CPSID I ; 关闭全局中断
BX LR ; 函数返回
PRIMASK
表示CM3内核中断屏蔽寄存器,置1后关闭所有可屏蔽中断的异常,只剩不可屏蔽中断NMI与硬(hard)fault中断可以响应。它的缺省值为0(没有关中断)。- 为了快速关中断,CM3还专门设置了一条CPS指令,用法如下:
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
r0寄存器存储的数据即为函数返回值,即为在调用rt_hw_interrupt_disable
函数之前PRIMASK
寄存器的值(关中断前的中断状态)。
注:中断可发生在 MRS r0, PRIMASK
和CPSID I
之间,但这并不会导致全局中断状态的错乱。
- 恢复全局中断
void rt_hw_interrupt_enable(rt_base_t level);
汇编实现代码:
.global rt_hw_interrupt_enable ; 全局声明,表示可以从其他文件访问
.type rt_hw_interrupt_enable, %function ; 伪指令定义函数
rt_hw_interrupt_enable:
MSR PRIMASK, R0 ; 将 r0 寄存器的值写入到 PRIMASK 寄存器
BX LR ; 函数返回
r0寄存器存储的数据为函数入口参数,使用 MSR 指令将 r0 的值写入到PRIMASK
寄存器,从而恢复之前的中断状态。
注:恢复中断一般和关闭中断成对使用,对于嵌套中断锁必须这样操作:
rt_base_t level1, level2;
level1 = rt_hw_interrupt_disable(); // level1=0, PRIMASK=1
/* 临界段1开始 */
level2 = rt_hw_interrupt_disable(); // level2=1, PRIMASK=1
/* 临界段2代码 */
rt_hw_interrupt_enable(level2); // level2=1, PRIMASK=1
/* 临界段1结束 */
rt_hw_interrupt_enable(level1); // level1=0, PRIMASK=0
每个中断锁需要用单独的level
变量记录各自关锁前的中断状态,然后操作完各自临界段后再恢复原来状态。如果直接开关中断,在临界段2操作完后中断就打开了,此时临界段1还未结束。
1.2 中断通知
当整个系统被中断打断,进入中断处理函数时,需要通知内核当前已经进入到中断状态。
- 进入中断通知函数接口
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
RT_OBJECT_HOOK_CALL(rt_interrupt_enter_hook,());
rt_hw_interrupt_enable(level);
}
- 退出中断通知函数接口
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
RT_OBJECT_HOOK_CALL(rt_interrupt_enter_hook,());
rt_hw_interrupt_enable(level);
}
可以看到这里使用了中断锁来保证代码的互斥运行,rt_interrupt_nest
表示中断嵌套深度。
- 每当进入中断时,可以调用 rt_interrupt_enter() 函数,用于通知内核,当前已经进入了中断状态,并增加中断嵌套深度(执行
rt_interrupt_nest
++) - 每当退出中断时,可以调用 rt_interrupt_leave() 函数,用于通知内核,当前已经离开了中断状态,并减少中断嵌套深度(执行
rt_interrupt_nest
--)。
注意:不要在应用程序中调用这两个接口函数。
- 获取中断嵌套深度值
rt_uint8_t rt_interrupt_get_nest(void)
{
rt_uint8_t ret;
rt_base_t level;
level = rt_hw_interrupt_disable();
ret = rt_interrupt_nest;
rt_hw_interrupt_enable(level);
return ret;
}
返回 | 描述 |
---|---|
0 | 当前系统不处于中断上下文环境中 |
1 | 当前系统处于中断上下文环境中 |
>1 | 前中断嵌套层次 |
中断通知函数接口主要作用:在中断服务程序中,如果使用了内核相关函数,则可以通过判断当前中断状态,让内核及时调整相应行为。 | |
如线程切换调度函数的使用(仅展示相关代码): |
void rt_schedule(void)
{
if (rt_interrupt_nest == 0)
{
rt_hw_context_switch((rt_ubase_t)&from_thread->sp,
(rt_ubase_t)&to_thread->sp);
}
else
{
rt_hw_context_switch_interrupt((rt_ubase_t)&from_thread->sp,
(rt_ubase_t)&to_thread->sp);
}
}
通过rt_interrupt_nest
判断线程当前环境,若值为0,直接在线程上下文中切换;若值不为0,则在中断上下文环境中切换。
注意:
- 如果中断服务程序不会调用内核相关的函数,也可以不调用
rt_interrupt_enter/leave()
函数。 - CM3/4中
rt_hw_context_switch
和rt_hw_context_switch_interrupt
函数实现一致,因为PendSV异常的优先级设为最低,当所有中断服务例程执行完后,才会执行PendSV异常服务例程,同时支持中断嵌套,硬件会自动压栈和弹栈,做存好寄存器中的数据。
1.3 使用示例
使用中断锁互斥保护临界资源num1
与num2
:
/*
* Date Author
* 2022-02-07 issac wan
*/
#include <rtthread.h>
#include <stdio.h>
#define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
static uint16_t num1 = 0, num2 = 0;
static void thread_entry1(void* param){
rt_uint32_t level;
while (1){
level = rt_hw_interrupt_disable(); // 关中断
num1++;
rt_thread_yield();
num2++;
rt_hw_interrupt_enable(level); // 开中断
}
}
static void thread_entry2(void* param){
rt_uint32_t level;
while (1){
level = rt_hw_interrupt_disable(); // 关中断
if (num1 == num2) // 两数相等说明保护成功
my_printf("Successful! num1:[%d], num2:[%d]", num1, num2);
else
my_printf("Fail! num1:[%d], num2:[%d]", num1, num2);
num1++;
num2++;
rt_hw_interrupt_enable(level); // 开中断
if (num1 > 50)
return;
}
}
int interrupt_sample(void){
rt_thread_t thread;
thread = rt_thread_create("thread1",
thread_entry1,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread);
thread = rt_thread_create("thread2",
thread_entry2,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread);
return RT_EOK;
}
INIT_APP_EXPORT(interrupt_sample);
未使用中断锁时,串口打印信息如下:
同步失败。
使用中断锁时,串口打印信息如下:
同步成功。
具体过程如下:首先运行线程1(虽然两线程优先级相同,但线程1先插入到线程就绪链表中),线程1执行完num1
加1后主动挂起线程,并发起线程调度,但是需要等待中断打开后才会跳转到线程2当中去执行。因两线程优先级相同,所以系统采用时间片轮询调度的方式来运行两线程,当线程2执行完5个时间片后,再切换到线程1执行。所以每执行5个时间片,数字会跳变。
2 调度锁
- 定义
调度锁与中断锁类似,只不过是采用关调度器的方式,不让线程进行切换,但是调度锁不会阻止系统的响应中断。
- 应用场合与主要事项
某线程想对资源独占访问(如共享内存资源)或 实时性要求很高的场合,防止被优先级更高的线程抢占,可以持调度锁,保证该线程以独占的方式执行完该资源,再允许线程调度。由于持调度锁后,不允许线程调度切换,因此需要“快进快出”。
在中断服务函数中也可以使用调度锁。
2.1 调度锁函数接口
不考虑多核处理器SMP
- 调度器上锁
void rt_enter_critical(void)
{
register rt_base_t level;
/* 关中断 */
level = rt_hw_interrupt_disable();
rt_scheduler_lock_nest ++;
/* 开中断 */
rt_hw_interrupt_enable(level);
}
rt_scheduler_lock_nest
表示调度锁深度,系统在调度器初始化函数rt_system_scheduler_init()
中会将该值置0。
- 调度器解锁
void rt_exit_critical(void)
{
register rt_base_t level;
/* 关中断 */
level = rt_hw_interrupt_disable();
rt_scheduler_lock_nest --;
if (rt_scheduler_lock_nest <= 0)
{
rt_scheduler_lock_nest = 0;
/* 开中断 */
rt_hw_interrupt_enable(level);
if (rt_current_thread) // 当前线程对象句柄,如果调度器还未启动,则返回RT_NULL
{
/* 如果调度程序已经启动,执行一次线程调度 */
rt_schedule();
}
}
else
{
/* 开中断 */
rt_hw_interrupt_enable(level);
}
}
如果调度器已经启动,在调度器解锁后,会执行一次线程调度。
注:线程调度锁的上锁与解锁必须成对使用。
- 获取调度锁的深度
rt_uint16_t rt_critical_level(void)
{
return rt_scheduler_lock_nest;
}
返回 | 描述 |
---|---|
0 | 可以进行线程调度切换 |
>=1 | 禁止线程调度切换 |
那么如何实现是否禁用线程调度切换的呢? |
void rt_schedule(void)
{
...
if (rt_scheduler_lock_nest == 0)
{
...
}
...
}
可以看到线程调度切换函数在执行相关操作前有个判断,即当rt_scheduler_lock_nest
为0时才执行。(关于rt_schedule
函数具体分析可参考线程时间片轮转调度实现)
2.2 使用示例
创建一个线程,通过延时进入空闲任务钩子,用于打印进入空闲钩子的次数,对执行次数全局变量用调度锁保护:
/* 空闲函数钩子函数执行次数 */
volatile static int hook_times = 0;
/* 空闲任务钩子函数 */
static void idle_hook()
{
if (0 == (hook_times % 10000))
{
my_printf("enter idle hook %d times.", hook_times);
}
rt_enter_critical(); // 调度器上锁
hook_times++;
rt_exit_critical(); // 调度器解锁
}
/* 线程入口 */
static void thread_entry(void *parameter)
{
int i = 5;
while (i--)
{
my_printf("enter thread1.");
hook_times = 0;
/* 休眠500ms */
my_printf("thread1 delay 500 OS Tick.");
rt_thread_mdelay(500);
}
my_printf("delete idle hook.");
/* 删除空闲钩子函数 */
rt_thread_idle_delhook(idle_hook);
my_printf("thread1 finish.");
}
int scheduler_lock_sample(void)
{
rt_thread_t thread;
/* 设置空闲线程钩子 */
rt_thread_idle_sethook(idle_hook);
/* 创建线程 */
thread = rt_thread_create("thread1",
thread_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread == RT_NULL)
return -RT_ENOMEM;
else
rt_thread_startup(thread);
return RT_EOK;
}
INIT_APP_EXPORT(scheduler_lock_sample);
串口部分打印信息如下:
3 死锁
- 定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。
- 死锁发生的原因
- 线程交叉获取对方资源而不释放。
如互斥量使用不当造成的死锁:
现有线程thread1与thread2,互斥锁x、y。系统运行后thread1和thread2分别占用互斥锁x和互斥锁y,执行一些指令后,thread1想获取互斥锁y,但它已被thread2占用,因此thread1挂起进入阻塞态,而后thread2想获取互斥锁x,又因它已被thread1占用,因此thread2也挂起进入阻塞态。这样两个任务互相等待对方持有的互斥量,被永久挂起形成死锁。
- 不具有递归访问特性的锁,强制执行递归调用操作。
如信号量递归调用:
假设信号量资源计数值初始化为1,即二值信号量。当线程第一次获取信号量之后没有释放,想再次获取,此时信号量资源已枯竭,获取失败被挂起,无法执行后面的释放信号量的操作,形成死锁。
void thread_entry(void *parameter)
{
while (1)
{
rt_sem_take(sem, RT_WAITING_FOREVER);
/* 执行临界区1 */
rt_sem_take(sem, RT_WAITING_FOREVER);
/* 执行临界区2 */
rt_sem_release(sem);
rt_sem_release(sem);
}
}
- 线程获得锁后,还没来得及释放锁,线程就被删除或关闭了。
如两个线程来回持有互斥锁,线程1执行5次后直接退出循环,有系统回收其资源,此时还没有来得及执行后面的释放互斥量的操作,线程2因无法获取该互斥量而不永久挂起。(注意:线程上下文中不能直接使用rt_thread_delete
函数删除自身)
#include <rtthread.h>
#include <stdio.h>
#include <rthw.h>
#define my_printf(fmt, ...) rt_kprintf ("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
static rt_mutex_t mutex = RT_NULL;
static rt_thread_t thread1 = RT_NULL, thread2 = RT_NULL;
static rt_uint8_t cnt = 0;
static void thread_entry1(void* param){
while (1){
rt_mutex_take(mutex, RT_WAITING_FOREVER);
if (++cnt == 5){
my_printf("delete thread1 !");
return;
}
my_printf("this is thread1");
rt_thread_mdelay(500);
rt_mutex_release(mutex);
}
}
static void thread_entry2(void* param){
while (1){
rt_mutex_take(mutex, RT_WAITING_FOREVER);
my_printf("this is thread2");
rt_thread_mdelay(500);
rt_mutex_release(mutex);
}
}
int dead_lock_sample(void){
mutex = rt_mutex_create("mutex", RT_IPC_FLAG_PRIO);
if (mutex == RT_NULL)
return -RT_ERROR;
thread1 = rt_thread_create("thread1",
thread_entry1,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread1 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread1);
thread2 = rt_thread_create("thread2",
thread_entry2,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread2 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread2);
return RT_EOK;
}
INIT_APP_EXPORT(dead_lock_sample);
4. 中断锁、调度锁不成对使用。
- 如何避免和解决死锁
- 情况一:顺序获取锁,不要交叉使用。
- 情况二:不具有递归访问特性的锁,不要递归调用,如信号量。
- 情况三:切记释放锁后再删除线程。
- 情况四:成对使用即可。
4 总结
- 中断锁效率最高,采用关中断的方式实现,可以用于任何场合,临界段遵循“快进快出”原则。中断锁中可以进行线程调度,但并不会立即执行,需要等待中断锁打开后系统才会执行调度。中断通知函数可以记录中断嵌套深度。中断锁也可以在中断里使用,防止被更高优先级的中断打断。
- 调度锁采用关调度器的方式实现,用一个全局变量记录调度锁的深度,常应用于线程对资源的独占访问或实现性要求较高的场合,防止被优先级更高的线程抢占而打断操作。与中断锁一样,调度锁持锁时间也不宜过长,以免影响系统实时性。调度锁可以在中断里使用,保护临界资源。
- 死锁的危害对系统而言是毁灭性的,一般是因为对锁的特性理解有误或操作不当造成的,使用时需注意本文提到的几种导致死锁发生的原因。
END