[007] [RT-Thread学习笔记] 中断锁、调度锁与死锁

RT-Thread
学习笔记
中断锁
中断锁函数接口
中断通知
使用示例
调度锁
调度锁函数接口
使用示例
死锁
总结

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, PRIMASKCPSID 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_switchrt_hw_context_switch_interrupt函数实现一致,因为PendSV异常的优先级设为最低,当所有中断服务例程执行完后,才会执行PendSV异常服务例程,同时支持中断嵌套,硬件会自动压栈和弹栈,做存好寄存器中的数据。

1.3 使用示例

使用中断锁互斥保护临界资源num1num2

/*
 * 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 死锁

  • 定义

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

系统发生死锁现象不仅浪费大量的系统资源,甚至导致整个系统崩溃,带来灾难性后果。

  • 死锁发生的原因
  1. 线程交叉获取对方资源而不释放。
    如互斥量使用不当造成的死锁:
    现有线程thread1与thread2,互斥锁x、y。系统运行后thread1和thread2分别占用互斥锁x和互斥锁y,执行一些指令后,thread1想获取互斥锁y,但它已被thread2占用,因此thread1挂起进入阻塞态,而后thread2想获取互斥锁x,又因它已被thread1占用,因此thread2也挂起进入阻塞态。这样两个任务互相等待对方持有的互斥量,被永久挂起形成死锁。
thread1 wants y_mutex
thread2 wants x_mutex
thread1
hold x_mutex
thread2
hold y_mutex
  1. 不具有递归访问特性的锁,强制执行递归调用操作。
    如信号量递归调用:
    假设信号量资源计数值初始化为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. 线程获得锁后,还没来得及释放锁,线程就被删除或关闭了。
    如两个线程来回持有互斥锁,线程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. 中断锁、调度锁不成对使用

  • 如何避免和解决死锁
  1. 情况一:顺序获取锁,不要交叉使用。
  2. 情况二:不具有递归访问特性的锁,不要递归调用,如信号量。
  3. 情况三:切记释放锁后再删除线程。
  4. 情况四:成对使用即可。

4 总结

  • 中断锁效率最高,采用关中断的方式实现,可以用于任何场合,临界段遵循“快进快出”原则。中断锁中可以进行线程调度,但并不会立即执行,需要等待中断锁打开后系统才会执行调度。中断通知函数可以记录中断嵌套深度。中断锁也可以在中断里使用,防止被更高优先级的中断打断。
  • 调度锁采用关调度器的方式实现,用一个全局变量记录调度锁的深度,常应用于线程对资源的独占访问或实现性要求较高的场合,防止被优先级更高的线程抢占而打断操作。与中断锁一样,调度锁持锁时间也不宜过长,以免影响系统实时性。调度锁可以在中断里使用,保护临界资源。
  • 死锁的危害对系统而言是毁灭性的,一般是因为对锁的特性理解有误或操作不当造成的,使用时需注意本文提到的几种导致死锁发生的原因。

END

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
死锁是指在一个系统中,多个进程或线程因竞争共享资源而导致的互相等待,无法继续执行的情况。当多个进程或线程相互等待对方释放资源时,系统陷入了死锁状态。 在rt-thread中,为了解决死锁问题,我们可以采取以下措施: 1. 避免使用嵌套:在编写代码时,尽量避免在一个的范围内再次申请其他。如果需要多个,可以尝试使用的层次结构,确保的获取和释放的顺序一致。 2. 按序申请和释放资源:在多个线程或进程之间共享资源时,需要按照一定的顺序来申请和释放资源,避免发生循环依赖。这样可以避免死锁的产生。 3. 加时限:在rt-thread中,可以设置的持有时间,如果某个的持有时间超过一定阈值,可以主动释放,并记录错误信息。这样可以减少死锁的发生频率,提高系统的可靠性。 4. 死锁检测和恢复:在rt-thread中,可以实现死锁检测机制,当检测到死锁时,可以采取一些恢复措施,如释放所有,并通知相关线程或进程重新开始执行。这样可以及时解决死锁问题,保证系统的正常运行。 5. 合理设计并发结构:在系统设计阶段,可以合理划分资源的使用范围,减少竞争,避免死锁的发生。可以使用分布式、读写等机制,来避免资源竞争引起的死锁问题。 综上所述,在rt-thread中解决死锁问题可以通过避免嵌套、按序申请和释放资源、加时限、死锁检测和恢复以及合理设计并发结构等措施来实现。通过这些方法,可以有效预防和解决死锁问题,提高系统的可靠性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柯西的彷徨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值