本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)
文章目录
互斥量的基本概念
互斥量又叫相互排斥的信号量,是一种特殊的二值性信号量。它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性。
互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。这个特性与一般的二值信号量有很大的不同,在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)。——RT-Thread官方中文手册
互斥量的优先级继承机制
使用信号量会导致的另一个潜在问题是线程优先级翻转。所谓优先级翻转问题即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。
优先级继承协议是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。
• 警告: 在获得互斥量后,请尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级。——RT-Thread官方中文手册
互斥量的应用场景
互斥量适用于:
- 线程可能会多次获取互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题;
- 可能会引起优先级翻转的情况。
典型例子:串口通讯时,由于硬件资源只有一个,如果两个线程需要同时发送,则必须加上互斥锁。
注意:互斥量不能在中断服务函数中使用。
互斥量的运作机制
互斥量相当于一把钥匙,两个线程想要获取同一个资源,必须先获取这个“钥匙”,当前一个线程释放“钥匙”后,下一个线程才能继续访问该资源。
互斥量控制块
struct rt_mutex
{
struct rt_ipc_object parent; /**< 继承自ipc_object类 */
rt_uint16_t value; /**< 互斥量的值 */
rt_uint8_t original_priority; /**< 持有线程的原始优先级 */
rt_uint8_t hold; /**< 持有线程的持有次数 */
struct rt_thread *owner; /**< 当前拥有互斥量的线程 */
};
互斥量函数接口介绍
创建互斥量 rt_mutex_create()
rt_mutex_t
rt_mutex_create
(const char* name, rt_uint8_t flag);
可以调用rt_mutex_create函数创建一个互斥量,它的名字有name所指定。创建的互斥量由于指定的flag不同,而有不同的意义: 使用PRIO优先级flag创建的IPC对象,在多个线程等待资源时,将由优先级高的线程优先获得资源。而使用FIFO先进先出flag创建的IPC对象,在多个线程等待资源时,将按照先来先得的顺序获得资源。
参数 | 描述 |
---|---|
name | 互斥量名称 |
flag | 互斥量标志 |
删除互斥量 rt_mutex_delete()
rt_err_t
rt_mutex_delete
(rt_mutex_t mutex);
当删除一个互斥量时,所有等待此互斥量的线程都将被唤醒,等待线程获得的返回值是-RT_ERROR。然后系统将该互斥量从内核对象管理器链表中删除并释放互斥量占用的内存空间。
参数 | 描述 |
---|---|
mutex | 互斥量对象的句柄 |
初始化互斥量 rt_mutex_init()
rt_err_t
rt_mutex_init
(rt_mutex_t mutex, const char* name, rt_uint8_t flag);
使用该函数接口时,需指定互斥量对象的句柄(即指向互斥量控制块的指针),互斥量名称以及互斥量标志。互斥量标志可用上面创建互斥量函数里提到的标志。
参数 | 描述 |
---|---|
mutex | 互斥量对象的句柄 |
name | 互斥量名称 |
flag | 互斥量标志 |
互斥量释放 rt_mutex_release()
rt_err_t
rt_mutex_release
(rt_mutex_t mutex);
使用该函数接口时,只有已经拥有互斥量控制权的线程才能释放它,每释放一次该互斥量,它的持有计数就减1。当该互斥量的持有计数为零时(即持有线程已经释放所有的持有操作),它变为可用,等待在该信号量上的线程将被唤醒。
参数 | 描述 |
---|---|
mutex | 互斥量对象的句柄 |
互斥量获取 rt_mutex_take()
rt_err_t
rt_mutex_take
(rt_mutex_t mutex, rt_int32_t time);
如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加1,当前线程也不会挂起等待。如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。
参数 | 描述 |
---|---|
mutex | 互斥量对象的句柄 |
time | 指定的等待时间,单位是操作系统时钟节拍(OS Tick) |
互斥量使用注意事项
注意事项:
- 两个线程不能同时持有同一个互斥量。
- 互斥量不能在中断服务程序中使用。
- 为了保证 RT-Thread 的实时性,应该避免获取互斥量后长时间不释放。
- 不能在线程持有互斥量的过程中对其调用 rt_thread_control() 等函数(修改线程优先级)。
互斥量实验
使用互斥量,需要在rtconfig.h
中开启相关配置。
这个实验参考官方中文文档,实验将创建3个动态线程,在低优先级线程获取到互斥量后,判断其优先级是否被调整为等待线程优先级中的最高优先级。
线程1,2,3的优先级分别是高,中和低,线程3先持有互斥量,然后让线程2去获取,此时线程2应该被调整为与线程2相同的优先级,这个检查过程在线程1中进行。
- 注:本实验线程3在获取到互斥量后进行了长时间的延时,实际使用时应该避免这种用法
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static rt_thread_t tid3 = RT_NULL;
// 定义互斥量控制块
static rt_mutex_t test_mux = RT_NULL;
/******************************************************************************
* @ 函数名 : thread1_entry
* @ 功 能 : 线程1入口
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void thread1_entry(void *parameter)
{
// 让低优先级线程先运行
rt_thread_delay(10);
// 此时线程3持有互斥量,并线程2等待互斥量
// 检查线程2与线程3的优先级情况
if(tid2->current_priority != tid3->current_priority)
{
// 优先级不相同,测试失败
rt_kprintf("线程3优先级未变化,测试失败!\n");
}
else
rt_kprintf("线程3优先级变为%d,测试成功!\n");
while(1)
{
rt_kprintf("线程2优先级:%d\n", tid2->current_priority);
rt_kprintf("线程3优先级:%d\n\n", tid3->current_priority);
rt_thread_delay(1000);
}
}
/******************************************************************************
* @ 函数名 : thread2_entry
* @ 功 能 : 线程2入口
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void thread2_entry(void *parameter)
{
rt_err_t result;
// 让低优先级线程3先运行
rt_thread_delay(5);
while(1)
{
// 试图获取互斥锁,此时线程3持有互斥锁,应把线程3优先级提升为线程2优先级
result = rt_mutex_take(test_mux, RT_WAITING_FOREVER);
if(result == RT_EOK)
{
rt_kprintf("线程2获取到互斥锁,且已释放\n");
// 释放互斥锁
rt_mutex_release(test_mux);
rt_thread_delay(800);
}
}
}
/******************************************************************************
* @ 函数名 : thread3_entry
* @ 功 能 : 线程3入口
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void thread3_entry(void *parameter)
{
rt_err_t result;
while(1)
{
// 获取互斥锁(测试连续获取两次)
result = rt_mutex_take(test_mux, RT_WAITING_FOREVER);
result = rt_mutex_take(test_mux, RT_WAITING_FOREVER);
if(result == RT_EOK)
{
rt_kprintf("线程3获取到互斥锁\n");
}
// 做一个长时间的挂起(实际使用时非常不建议这样用)
rt_thread_delay(1500);
rt_kprintf("线程3释放互斥锁\n");
// 释放互斥锁
rt_mutex_release(test_mux);
rt_mutex_release(test_mux);
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个互斥量
test_mux = // 互斥量控制块指针
rt_mutex_create("test_mux", // 互斥量名字 // 互斥量初始值
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_mux != RT_NULL)
rt_kprintf("互斥量创建成功!\n");
// 创建线程1
tid1 = // 线程控制块指针
rt_thread_create("tid1", // 线程名字
thread1_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
2, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(tid1 != RT_NULL)
rt_thread_startup(tid1);
else
return -1;
// 创建线程2
tid2 = // 线程控制块指针
rt_thread_create("tid2", // 线程名字
thread2_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
3, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(tid2 != RT_NULL)
rt_thread_startup(tid2);
else
return -1;
// 创建线程3
tid3 = // 线程控制块指针
rt_thread_create("tid3", // 线程名字
thread3_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
4, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(tid3 != RT_NULL)
rt_thread_startup(tid3);
else
return -1;
}
实验现象
当优先级最低的线程3持有互斥锁且线程2也开始获取互斥锁时,线程3的优先级提升为线程2的优先级。
当线程2释放互斥锁后使用delay函数挂起,此时线程3再次获取到互斥锁,但由于线程2没有在互斥量的等待队列中,所以线程3优先级不变。
当线程2 delay 结束,继续开始尝试获取互斥量,此时线程3优先级再次提升为线程2的优先级。