RT-Thread
学习笔记
互斥量概念
互斥量与信
号量的区别
自旋锁与互
斥锁的区别
优先级翻转问题
及其解决方法
互斥量控制块
互斥量函数接口
创建/删除
初始化/脱离
获取/无等待获取
释放互斥量
互斥量应用示例
互斥访问临
界资源实验
优先级继承特性实验
总结
RT-Thread版本:4.0.5 MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
1 互斥量概念
互斥量又名互斥锁、互斥型信号量,是一种特殊的二值信号量。 任意时刻互斥量的状态只有两种:
- 闭锁:互斥量被某线程持有,有且仅有该线程可以获得互斥量的所有权。
- 开锁:互斥量被该线程释放,同时该线程也失去了对此互斥量的所有权。
1.1 互斥量与信号量的区别
- 互斥量的线程拥有互斥量的所有权,互斥量只能由持有线程释放,信号量可以由任何线程释放;
- 互斥量支持递归访问且能防止线程优先级翻转,而信号量不可以;
- 二值信号量适用于实现同步(线程之间、线程与中断之间),互斥量也可以用于同步,但更多用于临界区资源的保护(即互斥)。
- 互斥量值只能为
0/1 ,信号量值一般为0-65535 。
1.2 自旋锁与互斥锁的区别
RT-Thread内核目前还没有自旋锁(spin lock)
自旋锁与互斥锁(量)类似,都是用于互斥操作临界资源,任何时刻仅能有一个执行单元(线程、中断)可以获得锁。两者在调度机制上有所不同:
- 对于互斥锁(
sleep-waiting 类型),若资源已被占用,资源申请者只能进入睡眠状态。 - 对于自旋锁(
busy-waiting 类型),不会让调用者睡眠,若自旋锁被别的执行单元保持,调用者会一直循环检测该自旋锁的保持者是否已释放了锁(相当于死循环检测标志位)。
综上可知,互斥锁的调用者在调用过程中会发生上下文切换(挂起态与运行态的转换),初始时间开销大;而自旋锁的调用者一直处于运行态,虽初始开销低于互斥锁,但会长期占用CPU资源。基于上述特点,它们有不同的应用场合:
- 互斥锁适用于临界区持锁时间比较长的操作,即临界区代码时间复杂度高或,因此只能在线程上下文中使用。
- 自旋锁适用于锁使用者保持锁时间比较短的情况,此时其效率远高于互斥锁,因此可以在任何上下文中使用。
注意:
- 自旋锁只有在内核可抢占式或SMP(多核处理器)的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。
- 互斥锁支持递归访问,而自旋锁不支持,当自旋锁的持有者试图再次获取该自旋锁时,会造成死锁,导致自身和其他申请该锁的线程不停地疯狂“自旋”,无法获取资源,陷入死循环。
此外,由于自旋锁的调用者不会进入睡眠态,因此就没有优先级翻转的问题。
2 优先级翻转问题及其解决方法
- 优先级翻转
信号量详解及应用一文中提到使用信号量会出现优先级翻转问题,具体原因如下:当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。
解决优先级翻转方法:
- 优先级继承
提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。 - 优先级天花板
直接将某个占有某种资源的低优先级线程的优先级,提升至当前系统所有线程中最高优先级的线程的优先级,该最高优先级即为优先级天花板。等该线程释放资源后,优先级重新回到初始设定。
两者区别:优先级继承只有当高优先级线程访问被已低优先级线程占用的临界资源时,才会提高低优先级线程的优先级,而优先级天花板是谁先占用资源,就将该线程优先级提升到当前系统最高优先级。
RT-Thread中互斥量采用优先级继承算法,以解决优先级翻转问题。所以对于临界资源的保护一般建议使用互斥量。
注意:获取互斥量后要尽快释放,并且在持有互斥量过程中,不得再更改持有互斥量线程的优先级,否则可能会人为造成优先级翻转的问题。
3 互斥量控制块
struct rt_mutex
{
struct rt_ipc_object parent;
rt_uint16_t value;
rt_uint8_t original_priority;
rt_uint8_t hold;
struct rt_thread *owner;
};
typedef struct rt_mutex* rt_mutex_t;
value :初始状态下互斥量的值为 1,因此,如果值大于 0,表示可以使用互斥量。original_priority :用来保存优先级继承时持有线程的原优先级hold :用于记录线程递归调用获取互斥量的次数owner :保存当前持有该互斥量线程的地址
4 互斥量函数接口
4.1 创建/删除
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)
{
struct rt_mutex *mutex;
(void)flag;
RT_DEBUG_NOT_IN_INTERRUPT;
mutex = (rt_mutex_t)rt_object_allocate(RT_Object_Class_Mutex, name);
if (mutex == RT_NULL)
return mutex;
_ipc_object_init(&(mutex->parent));
mutex->value = 1;
mutex->owner = RT_NULL;
mutex->original_priority = 0xFF;
mutex->hold = 0;
mutex->parent.parent.flag = RT_IPC_FLAG_PRIO;
return mutex;
}
互斥量的flag标志目前已经作废,仅能使用RT_IPC_FLAG_PRIO
rt_err_t rt_mutex_delete(rt_mutex_t mutex)
{
RT_DEBUG_NOT_IN_INTERRUPT;
RT_ASSERT(mutex != RT_NULL);
RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);
RT_ASSERT(rt_object_is_systemobject(&mutex->parent.parent) == RT_FALSE);
_ipc_list_resume_all(&(mutex->parent.suspend_thread));
rt_object_delete(&(mutex->parent.parent));
return RT_EOK;
}
当删除互斥量时,所有等待此互斥量的线程都将被唤醒,返回值为-RT_ERROR ,然后系统再将该互斥量从内核对象管理器链表中删除并释放内存。
4.2 初始化/脱离
静态初始化与脱离(静态对象不能被删除)互斥量对象,功能与上述函数一致,不再赘述。
4.3 获取/无等待获取
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
{
register rt_base_t temp;
struct rt_thread *thread;
RT_DEBUG_IN_THREAD_CONTEXT;
RT_ASSERT(mutex != RT_NULL);
RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(mutex->parent.parent)));
thread->error = RT_EOK;
if (mutex->owner == thread)
{
if(mutex->hold < RT_MUTEX_HOLD_MAX)
{
mutex->hold ++;
}
else
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
}
else
{
__again:
if (mutex->value > 0)
{
mutex->value --;
mutex->owner = thread;
mutex->original_priority = thread->current_priority;
if(mutex->hold < RT_MUTEX_HOLD_MAX)
{
mutex->hold ++;
}
else
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
}
else
{
if (time == 0)
{
thread->error = -RT_ETIMEOUT;
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
else
{
RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_take: suspend thread: %s\n",
thread->name));
if (thread->current_priority < mutex->owner->current_priority)
{
rt_thread_control(mutex->owner,
RT_THREAD_CTRL_CHANGE_PRIORITY,
&thread->current_priority);
}
_ipc_list_suspend(&(mutex->parent.suspend_thread),
thread,
mutex->parent.parent.flag);
if (time > 0)
{
RT_DEBUG_LOG(RT_DEBUG_IPC,
("mutex_take: start the timer of thread:%s\n",
thread->name));
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&time);
rt_timer_start(&(thread->thread_timer));
}
rt_hw_interrupt_enable(temp);
rt_schedule();
if (thread->error != RT_EOK)
{
if (thread->error == -RT_EINTR) goto __again;
return thread->error;
}
else
{
temp = rt_hw_interrupt_disable();
}
}
}
}
rt_hw_interrupt_enable(temp);
RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(mutex->parent.parent)));
return RT_EOK;
}
-
若互斥量已被当前线程占用,互斥量持有计数值mutex->hold 加1,且当前线程不会挂起等待; -
若互斥量未被其他线程占用,则被当前申请互斥量的线程成功获取,互斥量值mutex->value 从1->0,并记录该线程的地址及其原优先级,而后将mutex->hold 从0->1; -
若互斥量已被其他线程占用,即mutex->value 为0,则当前线程挂起等待,此时:
- time==0:直接返回超时错误码
- time>0:优先级继承算法在此体现:首先判断申请获取该互斥量线程与持有互斥量线程的当前优先级大小关系,如果申请线程优先级
thread->current_priority 高于 持有线程优先级mutex->owner->current_priority ,则将持有线程的优先级提高至申请线程的优先级。 然后挂起当前线程,设置它的定时器超时时间并启动定时器,最后发起任务调度。 -
无等待获取互斥量
rt_err_t rt_mutex_trytake(rt_mutex_t mutex)
{
return rt_mutex_take(mutex, RT_WAITING_NO);
}
即rt_mutex_take(rt_mutex_t mutex, 0)
4.4 释放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex)
{
register rt_base_t temp;
struct rt_thread *thread;
rt_bool_t need_schedule;
RT_ASSERT(mutex != RT_NULL);
RT_ASSERT(rt_object_get_type(&mutex->parent.parent) == RT_Object_Class_Mutex);
need_schedule = RT_FALSE;
RT_DEBUG_IN_THREAD_CONTEXT;
thread = rt_thread_self();
temp = rt_hw_interrupt_disable();
RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(mutex->parent.parent)));
if (thread != mutex->owner)
{
thread->error = -RT_ERROR;
rt_hw_interrupt_enable(temp);
return -RT_ERROR;
}
mutex->hold --;
if (mutex->hold == 0)
{
if (mutex->original_priority != mutex->owner->current_priority)
{
rt_thread_control(mutex->owner,
RT_THREAD_CTRL_CHANGE_PRIORITY,
&(mutex->original_priority));
}
if (!rt_list_isempty(&mutex->parent.suspend_thread))
{
thread = rt_list_entry(mutex->parent.suspend_thread.next,
struct rt_thread,
tlist);
RT_DEBUG_LOG(RT_DEBUG_IPC, ("mutex_release: resume thread: %s\n",
thread->name));
mutex->owner = thread;
mutex->original_priority = thread->current_priority;
if(mutex->hold < RT_MUTEX_HOLD_MAX)
{
mutex->hold ++;
}
else
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
_ipc_list_resume(&(mutex->parent.suspend_thread));
need_schedule = RT_TRUE;
}
else
{
if(mutex->value < RT_MUTEX_VALUE_MAX)
{
mutex->value ++;
}
else
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
mutex->owner = RT_NULL;
mutex->original_priority = 0xff;
}
}
rt_hw_interrupt_enable(temp);
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
若调用释放互斥量函数的线程不是持有该互斥量的线程,直接返回-RT_ERROR ;若是则将mutex->hold 减1,当mutex->hold 减到0时:
- 判断互斥量对象中保存的持有线程的原优先级与其当前优先级是否一致(即是否发生优先级继承),若一致则将持有线程的优先级恢复至其原优先级。
- 判断该互斥量等待队列上是否有挂起的线程,若有则将互斥量对象的持有线程切换至等待队列上的第一个线程(按优先级排序),并保存其原优先级,然后让
mutex->hold 从0->1,最后唤醒该线程,发生一次任务调度;若没有直接让mutex->value 从0->1,然后清空该互斥量对象信息(持有线程清为RT_NULL ,保存优先级设为0xff )。
5 互斥量应用示例
5.1 互斥访问临界资源实验
#include <rtthread.h>
#define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 5
static uint16_t num1 = 0, num2 = 0;
static rt_mutex_t mutex = RT_NULL;
static rt_thread_t thread1 = RT_NULL;
static rt_thread_t thread2 = RT_NULL;
void thread_entry1(void* param){
while (1){
rt_mutex_take(mutex, RT_WAITING_FOREVER);
num1++;
rt_thread_delay(10);
num2++;
rt_mutex_release(mutex);
}
}
void thread_entry2(void* param){
while (1){
rt_mutex_take(mutex, RT_WAITING_FOREVER);
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_thread_delay(1);
rt_mutex_release(mutex);
if (num1 > 50)
return;
}
}
int mutex_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,
512,
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,
512,
THREAD_PRIORITY-1,
THREAD_TIMESLICE);
if (thread2 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread2);
return RT_EOK;
}
INIT_APP_EXPORT(mutex_sample);
- 创建一个互斥量,用于保护临界资源num1和num2,使各线程互斥访问该临界资源。
- 创建两个线程:线程1和线程2,线程2优先级高于线程1。线程1对两数分别进行加1操作;线程2中会判断两数是否相等,相等则表示互斥量保护临界资源成功,然后再对两数进行加1操作。
本实验引用官方例程,但官方没有在线程2的入口函数中加延时,关于是否应加延时在此做个讨论。
-
若不加延时,则线程2一直处于运行态,而线程1优先级比其低,因此无法抢占CPU资源,不会执行获取互斥量的操作,也就不能将线程1挂在互斥量等待队列中。所以当线程2执行时,互斥量等待队列为空,直接让mutex->value加1,然后清空互斥量,恢复初始开锁状态,线程2可再次获取该互斥量,以此循环直至num加到50,系统回收线程2后,线程1才能抢占到CPU资源,执行其入口函数。 串口打印信息如下: 结果与官方一致,但实际上线程1在线程2回收后才开始执行,保护了个寂寞。 -
若加延时,会挂起线程2一段时间,线程1得以抢占到CPU资源,若获取互斥量失败,则将其挂在互斥量等待队列上,当线程2休眠结束后,执行开锁操作时,系统会将互斥量持有线程切换到线程1,然后发起一次任务调度(因当前运行的线程2其优先级最高,所以还是会继续执行线程2,直到它执行获取互斥量操作时,因获取失败而被挂起,此时才会切换到线程1)。 串口打印信息如下: 使用互斥量保护临界资源num1和num2成功。
5.2 优先级继承特性实验
#include <rtthread.h>
#define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 5
static rt_mutex_t mutex = RT_NULL;
static rt_thread_t thread1 = RT_NULL;
static rt_thread_t thread2 = RT_NULL;
static void thread_entry1(void* param){
while (1){
rt_thread_mdelay(50);
rt_mutex_take(mutex, RT_WAITING_FOREVER);
return;
}
}
static void thread_entry2(void* param){
while (1){
my_printf("优先级继承实验开始!");
rt_mutex_take(mutex, RT_WAITING_FOREVER);
my_printf("继承前的优先级:");
my_printf("the priority of thread1 is: %d", thread1->current_priority);
my_printf("the priority of thread2 is: %d", thread2->current_priority);
rt_thread_mdelay(100);
my_printf("继承后的优先级:");
if (thread1->current_priority != thread2->current_priority){
my_printf("the priority of thread1 is: %d", thread1->current_priority);
my_printf("the priority of thread2 is: %d", thread2->current_priority);
my_printf("测试失败!");
}else{
my_printf("the priority of thread1 is: %d", thread1->current_priority);
my_printf("the priority of thread2 is: %d", thread2->current_priority);
my_printf("测试成功!");
}
rt_mutex_release(mutex);
my_printf("恢复原优先级:");
my_printf("the priority of thread1 is: %d", thread1->current_priority);
my_printf("the priority of thread2 is: %d", thread2->current_priority);
my_printf("优先级继承实验结束!");
return;
}
}
int mutex_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 - 1,
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(mutex_sample);
- 首先,创建两个线程:线程1与线程2,线程1的优先级高于线程2;
- 先让线程1执行50ms延时,让线程2先成功获取互斥量,然后打印出优先级继承前的两线程优先级(主要看线程2的优先级,线程1不会发生优先级继承),再延时100ms,等待线程1获取互斥量;
- 当线程1获取互斥量时,因其已被线程2持有,获取失败,由于线程1优先级高于线程2,所以发生优先级继承,将线程2的优先级调整到等待线程优先级中最高的优先级(即线程1的优先级);
- 线程2延时结束后,会打印继承后的优先级,然后释放互斥量,因线程2发生了优先级继承,所以释放时会将其优先级改为原优先级,最后打印恢复后的优先级。
串口打印信息如下: 可以看到线程2继承前的优先级为20,继承后优先级提升至线程1的优先级19,在成功释放互斥量后恢复原优先级20。
6 总结
本文主要介绍了互斥量的特性,以及它与信号量、自旋锁的区别,然后阐述了优先级翻转问题带来的危害与解决方法,RT-Thread利用互斥量的优先级继承特性来解决该问题。其次详细分析了RT-Thread中互斥量控制块与其常用函数接口的原理与使用方法,最后通过两个实验来测试互斥量的两个常用使用场合:互斥保护临界区资源和多线程同步引发的优先级翻转问题。
END
|