一、互斥锁理论介绍
在学习 OneOS 的信号量中,会发现使用 OneOS 信号量有可能会出现优先级翻转现场,互斥锁是一种任务间互斥的机制,一个任务占有了某个资源,就不允许别的任务去访问,直到占有资源的任务释放锁。即一个资源同时只允许一个访问者对其访问,具有唯一性和排他性,但互斥不会限制访问者对资源的访问顺序,即访问是无序的。
1.1、互斥锁简介
互斥锁其实就是一个拥有优先级继承的信号量,在同步的应用中(任务与任务或中断与任务之间的同步)信号量最适合。互斥锁适合用于那些需要互斥访问的应用中。在互斥访问中互斥锁相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。互斥锁使用和信号量相同的 API 操作函数,所以互斥锁也可以设置阻塞时间,不同于信号量的是互斥锁具有优先级继承的特性。
当一个互斥锁正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥锁的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与 自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于 阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥锁不能用于中断服务函数中,原因如下:
(1)互斥锁有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
(2)中断服务函数中不能因为要等待互斥锁而设置阻塞时间进入阻塞态。
OneOS 系统的互斥锁支持非递归锁和递归锁两种形式:
当互斥锁设置为非递归锁时,一旦该锁被某个任务获取,在释放之前不能被任何任务再次获取;当互斥锁设置为递归锁时,若锁被某个任务获取,那么该任务可以再次获取这个锁而不会被挂起。
一般情况下,使用者在使用锁时,应该明确自己要保护的临界资源的范围,只在对临界资源访问时加锁,访问完成后立即解锁。对临界资源的访问,经过合理设计后,一般都可以使用非递归锁实现;递归锁在某些错综复杂的调用关系情况下,使用起来比较方便,但是容易隐藏代码中可能存在的问题。
1.2、互斥锁实现原理
互斥锁是基于阻塞队列实现,互斥锁的初始计数值为 0(代表此时互斥锁没有被获取),任务成功获取互斥锁时,计数值加 1(代表此时互斥锁已经被获取),并且该任务成为锁的持有者,当另一个任务获取该互斥锁时,由于锁已经被获取,该任务被挂起放到阻塞队列,直到锁的持有者释放锁,被挂起的任务才被唤醒并放到就绪队列。具体原理如下图所示:
图中(1),任务 1 先运行,并获取互斥锁 图中(2),任务 2 就绪运行
图中(3),任务 2 获取互斥锁,由于互斥锁已经被任务 1 持有,任务 2 获取失败
图中(4),任务 2 被阻塞,被放到阻塞队列 图中(5),任务 2 被阻塞,任务 1 运行
图中(6),任务 1 释放互斥锁,唤醒任务 2 图中(7),任务 2 被放到就绪队列
图中(8),任务 2 运行
1.2.1、互斥锁解决优先级翻转原理
为了避免优先级反转这个问题,OneOS 支持一种特殊的信号量:互斥信号量,用它可以解决优先级反转问题,如下图所示:
(1) 任务 H 与任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行中。
(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应资源的互斥锁。
(3) 任务 L 获得互斥锁并开始使用该共享资源。
(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。
(5) 任务 H 开始运行。
(6) 任务 H 运行过程中也要使用任务 L 在使用的资源,考虑到任务 L 正在占用着资源,OneOS 会将任务 L 的优先级升至同任务 H 一样,使得任务 L 能继续执行而不被其他中等优先级的任务打断。
(7) 任务 L 以任务 H 的优先级继续运行,注意此时任务 H 并没有运行,因为任务 H 在等待任务 L 释放掉互斥锁。
(8) 任务 L 完成所有的任务,并释放掉互斥锁,OneOS 会自动将任务 L 的优先级恢复到提升之前的值,然后 OneOS 会将互斥锁给正在等待着的任务 H。
(9) 任务 H 获得互斥锁开始执行。
(10) 任务 H 不再需要访问共享资源,于是释放掉互斥锁。
(11) 由于没有更高优先级的任务需要执行,所以任务 H 继续执行。
(12) 任务 H 完成所有工作,并等待某一事件发生,此时 OneOS 开始运行在任务 H 或者任务 L 运行过程中已经就绪的任务 M。
(13)任务 M 继续执行。
上面提到的方法是解决优先级翻转的常见方法,Oneos 针对任务持多个互斥锁的情况,在 恢复优先级时做了优化,如下图,释放互斥锁的时候,不是直接恢复持有该互斥锁任务的原始 优先级,而是遍历该任务的持有互斥锁队列,获取到队列上每个互斥锁阻塞队列上的第一个任 务(这个任务在该队列上优先级最高),然后取所有阻塞任务的最高优先级,将互斥锁任务恢复 为该优先级,如下图所示:
1.3、API函数介绍
1.3.1、互斥锁控制块
1.3.2、创建互斥锁
函数_k_mutex_init(精简) 申请内存和初始化成员变量
申请内存:
初始化成员变量:
1.3.3、获取互斥锁(非递归)
函数os_mutex_lock(精简)——锁定次数为0 更新锁定次数 设置锁定者
锁定次数不为0 优先级继承 阻塞任务
1.3.4、释放互斥锁(非递归)
函数os_mutex_unlock(精简)——阻塞队列为空 更新成员变量
阻塞队列不为空 唤醒阻塞任务 更新成员变量
1.3.5、销毁互斥锁
函数os_mutex_destroy(精简) 反向初始化成员变量 释放内存空间
反向初始化成员变量
二、互斥锁实验
下面代码展示了在OneOS操作系统中如何使用互斥锁来同步两个任务对共享资源(计数器count1和count2)的访问。以下是代码的执行逻辑:
执行逻辑:
- 初始化互斥锁: 在mutex_static_sample函数中,首先创建一个名为mutex_static的互斥锁。
- 创建任务: 创建两个任务:task1和task2,它们分别执行task1_entry和task2_entry函数。
- 启动任务: 启动task1和task2,使它们开始执行。
- 任务执行: 每个任务在其入口函数中进入一个无限循环,尝试获取互斥锁,修改共享资源,然后释放互斥锁。
- 获取互斥锁: 任务尝试获取互斥锁。如果成功,它将执行临界区代码;如果失败,它将记录错误日志。
- 修改共享资源: 任务在获取互斥锁后修改共享资源(计数器),然后记录日志。
- 休眠: 任务在修改共享资源后休眠一段时间,模拟工作负载。
- 释放互斥锁: 任务在休眠结束后释放互斥锁,并记录日志。
- 循环继续: 任务在释放互斥锁后继续循环,再次尝试获取互斥锁。
2.1、代码及其解释
#include <oneos_config.h> // 包含OneOS操作系统的配置头文件
#include <os_task.h> // 包含任务管理相关的头文件
#include <shell.h> // 包含Shell命令相关的头文件
#include <os_mutex.h> // 包含互斥锁管理相关的头文件
#include <dlog.h> // 包含日志系统相关的头文件
#define TEST_TAG "TEST" // 定义日志标签
#define TASK_STACK_SIZE 1024 // 定义任务堆栈大小
#define TASK1_PRIORITY 15 // 定义任务1的优先级
#define TASK2_PRIORITY 16 // 定义任务2的优先级
static uint32_t count1 = 0; // 定义任务1的计数器
static uint32_t count2 = 0; // 定义任务2的计数器
static os_mutex_id mutex_static = OS_NULL; // 定义互斥锁的ID,初始为NULL
static os_mutex_dummy_t mutex_cb; // 定义互斥锁的控制块
void task1_entry(void *para)
{
while (1)
{
if (OS_SUCCESS == os_mutex_lock(mutex_static, OS_WAIT_FOREVER)) // 尝试获取互斥锁
{
LOG_W(TEST_TAG, "task1 mutex lock"); // 获取互斥锁成功,记录日志
}
else
{
LOG_W(TEST_TAG, "task1 mutex lock err"); // 获取互斥锁失败,记录日志
}
count1++; // 任务1增加计数器
LOG_W(TEST_TAG, "task1 sleep"); // 记录日志
os_task_msleep(100); // 任务1休眠100毫秒
count2++; // 任务1增加计数器
if (OS_SUCCESS == os_mutex_unlock(mutex_static)) // 尝试释放互斥锁
{
LOG_W(TEST_TAG, "task1 mutex unlock"); // 释放互斥锁成功,记录日志
}
else
{
LOG_W(TEST_TAG, "task1 mutex unlock err"); // 释放互斥锁失败,记录日志
}
os_task_msleep(500); // 任务1休眠500毫秒
}
}
void task2_entry(void *para)
{
while (1)
{
if (OS_SUCCESS == os_mutex_lock(mutex_static, OS_WAIT_FOREVER)) // 尝试获取互斥锁
{
LOG_W(TEST_TAG, "task2 mutex lock"); // 获取互斥锁成功,记录日志
}
else
{
LOG_W(TEST_TAG, "task2 mutex lock err"); // 获取互斥锁失败,记录日志
}
LOG_W(TEST_TAG, "task2 count1:%d count2:%d", count1, count2); // 记录当前计数器的值
count1++; // 任务2增加计数器
count2++; // 任务2增加计数器
if (OS_SUCCESS == os_mutex_unlock(mutex_static)) // 尝试释放互斥锁
{
LOG_W(TEST_TAG, "task2 mutex unlock"); // 释放互斥锁成功,记录日志
}
else
{
LOG_W(TEST_TAG, "task2 mutex unlock err"); // 释放互斥锁失败,记录日志
}
os_task_msleep(500); // 任务2休眠500毫秒
}
}
void mutex_static_sample(void)
{
os_task_id task1;
os_task_id task2;
mutex_static = os_mutex_create(&mutex_cb, "mutex_static", OS_FALSE); // 创建互斥锁
task1 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task1", task1_entry, OS_NULL, TASK1_PRIORITY); // 创建任务1
if (task1)
{
os_task_startup(task1); // 启动任务1
}
task2 = os_task_create(OS_NULL, OS_NULL, TASK_STACK_SIZE, "task2", task2_entry, OS_NULL, TASK2_PRIORITY); // 创建任务2
if (task2)
{
os_task_startup(task2); // 启动任务2
}
}
int main()
{
mutex_static_sample(); // 调用互斥锁示例函数
}
SH_CMD_EXPORT(static_mutex, mutex_static_sample, "test staitc mutex"); // 将互斥锁示例函数导出为Shell命令
2.2、实验结果及其分析
从实验结果来看,其结果完全符合代码逻辑。这段代码通过使用互斥锁来同步两个任务对共享资源的访问,展示了互斥锁在多任务环境中确保数据一致性和线程安全的重要性。通过记录日志,我们可以追踪任务获取和释放互斥锁的状态,以及它们对共享资源的修改。这种同步机制是操作系统中实现任务协调和资源共享的关键技术。
2.3、自己的理解
在互斥锁已经被任务1获取的时候,就算任务1设置了休眠时间,但是任务2还是运行不了,因为任务2的条件也需要互斥锁,所以打印的结果就是任务1里面的打印输出和任务2交替打印,表面上看这一段代码中设置的休眠和优先级根本没用,任务调度好像也没有发挥作用,但是其实都是发挥了作用的只是在此处任务要运行打印必须要获取互斥锁,如果在任务二判定是否获取互斥锁之前,添加一句打印输出语句,其实是可以打印的。表明任务调度,优先级和休眠机制并没有出错。大家可以去调一调代码试一下。