互斥锁
基本概念
互斥锁又称互斥型信号量,用于实现对共享资源的独占式处理。当有任务持有时,这个任务获得该互斥锁的所有权。当该任务释放它时,任务失去该互斥锁的所有权。当一个任务持有互斥锁时,其他任务将不能再持有该互斥锁。多任务环境下往往存在多个任务竞争同一共享资源的应用场景,互斥锁可被用于对共享资源的保护从而实现独占式访问。
互斥锁属性包含3个属性:协议属性、优先级上限属性和类型属性。协议属性用于处理不同优先级的任务申请互斥锁,协议属性包含如下三种:
-
LOS_MUX_PRIO_NONE 不对申请互斥锁的任务的优先级进行继承或保护操作。
-
LOS_MUX_PRIO_INHERIT 优先级继承属性,默认设置为该属性,对申请互斥锁的任务的优先级进行继承。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果高优先级任务阻塞于互斥锁,则把持有互斥锁任务的优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为和高优先级任务相同的优先级;持有互斥锁的任务释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。
-
LOS_MUX_PRIO_PROTECT 优先级保护属性,对申请互斥锁的任务的优先级进行保护。在互斥锁设置为本协议属性情况下,申请互斥锁时,如果任务优先级小于互斥锁优先级上限,则把任务优先级备份到任务控制块的优先级位图中,然后把任务优先级设置为互斥锁优先级上限属性值;释放互斥锁时,从任务控制块的优先级位图恢复任务优先级。
互斥锁的类型属性用于标记是否检测死锁,是否支持递归持有,类型属性包含如下三种:
-
LOS_MUX_NORMAL 普通互斥锁,不会检测死锁。如果任务试图对一个互斥锁重复持有,将会引起这个线程的死锁。如果试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图重复释放互斥锁都会引发不可预料的结果。
-
LOS_MUX_RECURSIVE 递归互斥锁,默认设置为该属性。在互斥锁设置为本类型属性情况下,允许同一个任务对互斥锁进行多次持有锁,持有锁次数和释放锁次数相同,其他任务才能持有该互斥锁。如果试图持有已经被其他任务持有的互斥锁,或者如果试图释放已经被释放的互斥锁,会返回错误码。
-
LOS_MUX_ERRORCHECK 错误检测互斥锁,会自动检测死锁。在互斥锁设置为本类型属性情况下,如果任务试图对一个互斥锁重复持有,或者试图释放一个由别的任务持有的互斥锁,或者如果一个任务试图释放已经被释放的互斥锁,都会返回错误码。
运行机制
多任务环境下会存在多个任务访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理。互斥锁怎样来避免这种冲突呢?
用互斥锁处理非共享资源的同步访问时,如果有任务访问该资源,则互斥锁为加锁状态。此时其他任务如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的任务释放后,其他任务才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。
图1 小型系统互斥锁运作示意图
开发指导
接口说明
表1 互斥锁模块接口
功能分类 | 接口描述 |
---|---|
初始化和销毁互斥锁 | - LOS_MuxInit:互斥锁初始化 - LOS_MuxDestroy:销毁指定的互斥锁 |
互斥锁的申请和释放 | - LOS_MuxLock:申请指定的互斥锁 - LOS_MuxTrylock:尝试申请指定的互斥锁,不阻塞 - LOS_MuxUnlock:释放指定的互斥锁 |
校验互斥锁 | - LOS_MuxIsValid:判断互斥锁释放有效 - LOS_MuxAttrDestroy:销毁指定的互斥锁属性 |
设置和获取互斥锁属性 | - LOS_MuxAttrGetType:获取指定互斥锁属性的类型属性 - LOS_MuxAttrSetType:设置指定互斥锁属性的类型属性 - LOS_MuxAttrGetProtocol:获取指定互斥锁属性的协议属性 - LOS_MuxAttrSetProtocol:设置指定互斥锁属性的协议属性 - LOS_MuxAttrGetPrioceiling:获取指定互斥锁属性的优先级上限属性 - LOS_MuxAttrSetPrioceiling:设置指定互斥锁属性的优先级上限属性 - LOS_MuxGetPrioceiling:获取互斥锁优先级上限属性 - LOS_MuxSetPrioceiling:设置互斥锁优先级上限属性 |
开发流程
互斥锁典型场景的开发流程:
-
初始化互斥锁LOS_MuxInit。
-
申请互斥锁LOS_MuxLock。
申请模式有三种:无阻塞模式、永久阻塞模式、定时阻塞模式。
-
无阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有任务持有,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功;
-
永久阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则,该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行;
-
定时阻塞模式:任务需要申请互斥锁,若该互斥锁当前没有被占用,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级高者继续执行。任务进入阻塞态后,指定时间超时前有其他任务释放该互斥锁,或者用 户指定时间超时后,阻塞任务才会重新得以执行。
- 释放互斥锁LOS_MuxUnlock。
-
如果有任务阻塞于指定互斥锁,则唤醒被阻塞任务中优先级高的,该任务进入就绪态,并进行任务调度;
-
如果没有任务阻塞于指定互斥锁,则互斥锁释放成功。
- 销毁互斥锁LOS_MuxDestroy。
说明:
两个任务不能对同一把互斥锁加锁。如果某任务对已被持有的互斥锁加锁,则该任务会被挂起,直到持有该锁的任务对互斥锁解锁,才能执行对这把互斥锁的加锁操作。
互斥锁不能在中断服务程序中使用。
LiteOS-A内核作为实时操作系统需要保证任务调度的实时性,尽量避免任务的长时间阻塞,因此在获得互斥锁之后,应该尽快释放互斥锁。
编程实例
实例描述
本实例实现如下流程:
-
任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度。
-
Example_MutexTask2被调度,以永久阻塞模式申请互斥锁,并成功获取到该互斥锁,然后任务休眠100Tick,Example_MutexTask2挂起,Example_MutexTask1被唤醒。
-
Example_MutexTask1以定时阻塞模式申请互斥锁,等待时间为10Tick,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。10Tick超时时间到达后,Example_MutexTask1被唤醒,以永久阻塞模式申请互斥锁,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。
-
100Tick休眠时间到达后,Example_MutexTask2被唤醒, 释放互斥锁,唤醒Example_MutexTask1。Example_MutexTask1成功获取到互斥锁后,释放,删除互斥锁。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数Example_MutexEntry。
示例代码如下:
#include <string.h>
#include "los_mux.h"
/* 互斥锁 */
LosMux g_testMutex;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_MutexTask1(VOID)
{
UINT32 ret;
LOS_TaskDelay(50);
dprintf("task1 try to get mutex, wait 10 ticks.\n");
/* 申请互斥锁 */
ret = LOS_MuxLock(&g_testMutex, 10);
if (ret == LOS_OK) {
dprintf("task1 get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
return;
}
if (ret == LOS_ETIMEDOUT) {
dprintf("task1 timeout and try to get mutex, wait forever.\n");
/* 申请互斥锁 */
ret = LOS_MuxLock(&g_testMutex, LOS_WAIT_FOREVER);
if (ret == LOS_OK) {
dprintf("task1 wait forever, get mutex g_testMux.\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
/* 删除互斥锁 */
LOS_MuxDestroy(&g_testMutex);
dprintf("task1 post and delete mutex g_testMux.\n");
return;
}
}
return;
}
VOID Example_MutexTask2(VOID)
{
dprintf("task2 try to get mutex, wait forever.\n");
/* 申请互斥锁 */
(VOID)LOS_MuxLock(&g_testMutex, LOS_WAIT_FOREVER);
dprintf("task2 get mutex g_testMux and suspend 100 ticks.\n");
/* 任务休眠100Ticks */
LOS_TaskDelay(100);
dprintf("task2 resumed and post the g_testMux\n");
/* 释放互斥锁 */
LOS_MuxUnlock(&g_testMutex);
return;
}
UINT32 Example_MutexEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S task1;
TSK_INIT_PARAM_S task2;
/* 初始化互斥锁 */
LOS_MuxInit(&g_testMutex, NULL);
/* 锁任务调度 */
LOS_TaskLock();
/* 创建任务1 */
memset(&task1, 0, sizeof(TSK_INIT_PARAM_S));
task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask1;
task1.pcName = "MutexTsk1";
task1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task1.usTaskPrio = 5;
ret = LOS_TaskCreate(&g_testTaskId01, &task1);
if (ret != LOS_OK) {
dprintf("task1 create failed.\n");
return LOS_NOK;
}
/* 创建任务2 */
memset(&task2, 0, sizeof(TSK_INIT_PARAM_S));
task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask2;
task2.pcName = "MutexTsk2";
task2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
task2.usTaskPrio = 4;
ret = LOS_TaskCreate(&g_testTaskId02, &task2);
if (ret != LOS_OK) {
dprintf("task2 create failed.\n");
return LOS_NOK;
}
/* 解锁任务调度 */
LOS_TaskUnlock();
return LOS_OK;
}
结果验证
编译运行得到的结果为:
task2 try to get mutex, wait forever.
task2 get mutex g_testMux and suspend 100 ticks.
task1 try to get mutex, wait 10 ticks.
task1 timeout and try to get mutex, wait forever.
task2 resumed and post the g_testMux
task1 wait forever, get mutex g_testMux.
task1 post and delete mutex g_testMux.
消息队列
基本概念
队列又称消息队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。
任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。
可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将读队列和写队列的超时时间设置为大于0的时间,就会以阻塞模式运行。
消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:
- 消息以先进先出的方式排队,支持异步读写。
- 读队列和写队列都支持超时机制。
- 每读取一条消息,就会将该消息节点设置为空闲。
- 发送消息类型由通信双方约定,可以允许不同长度(不超过队列的消息节点大小)的消息。
- 一个任务能够从任意一个消息队列接收和发送消息。
- 多个任务能够从同一个消息队列接收和发送消息。
- 创建队列时所需的队列空间,接口内系统自行动态申请内存。
运行机制
队列控制块
/**
* 队列控制块数据结构
*/
typedef struct {
UINT8 *queueHandle; /**< Pointer to a queue handle */
UINT16 queueState; /**< Queue state */
UINT16 queueLen; /**< Queue length */
UINT16 queueSize; /**< Node size */
UINT32 queueID; /**< queueID */
UINT16 queueHead; /**< Node head */
UINT16 queueTail; /**< Node tail */
UINT16 readWriteableCnt[OS_QUEUE_N_RW]; /**< Count of readable or writable resources, 0:readable, 1:writable */
LOS_DL_LIST readWriteList[OS_QUEUE_N_RW]; /**< the linked list to be read or written, 0:readlist, 1:writelist */
LOS_DL_LIST memList; /**< Pointer to the memory linked list */
} LosQueueCB;
每个队列控制块中都含有队列状态,表示该队列的使用情况:
-
OS_QUEUE_UNUSED:队列未被使用。
-
OS_QUEUE_INUSED:队列被使用中。
队列运作原理
-
创建队列时,创建队列成功会返回队列ID。
-
在队列控制块中维护着一个消息头节点位置Head和一个消息尾节点位置Tail,用于表示当前队列中消息的存储情况。Head表示队列中被占用的消息节点的起始位置。Tail表示被占用的消息节点的结束位置,也是空闲消息节点的起始位置。队列刚创建时,Head和Tail均指向队列起始位置。
-
写队列时,根据readWriteableCnt[1]判断队列是否可以写入,不能对已满(readWriteableCnt[1]为0)队列进行写操作。写队列支持两种写入方式:向队列尾节点写入,也可以向队列头节点写入。尾节点写入时,根据Tail找到起始空闲消息节点作为数据写入对象,如果Tail已经指向队列尾部则采用回卷方式。头节点写入时,将Head的前一个节点作为数据写入对象,如果Head指向队列起始位置则采用回卷方式。
-
读队列时,根据readWriteableCnt[0]判断队列是否有消息需要读取,对全部空闲(readWriteableCnt[0]为0)队列进行读操作会引起任务挂起。如果队列可以读取消息,则根据Head找到最先写入队列的消息节点进行读取。如果Head已经指向队列尾部则采用回卷方式。
-
删除队列时,根据队列ID找到对应队列,把队列状态置为未使用,把队列控制块置为初始状态,并释放队列所占内存。
图1 队列读写数据操作示意图
上图对读写队列做了示意,图中只画了尾节点写入方式,没有画头节点写入,但是两者是类似的。
开发指导
接口说明
功能分类 | 接口描述 |
---|---|
创建/删除消息队列 | - LOS_QueueCreate:创建一个消息队列,由系统动态申请队列空间 - LOS_QueueDelete:根据队列ID删除一个指定队列 |
读/写队列(不带拷贝) | - LOS_QueueRead:读取指定队列头节点中的数据(队列节点中的数据实际上是一个地址) - LOS_QueueWrite:向指定队列尾节点中写入入参bufferAddr的值(即buffer的地址) - LOS_QueueWriteHead:向指定队列头节点中写入入参bufferAddr的值(即buffer的地址) |
读/写队列(带拷贝) | - LOS_QueueReadCopy:读取指定队列头节点中的数据 - LOS_QueueWriteCopy:向指定队列尾节点中写入入参bufferAddr中保存的数据 - LOS_QueueWriteHeadCopy:向指定队列头节点中写入入参bufferAddr中保存的数据 |
获取队列信息 | LOS_QueueInfoGet:获取指定队列的信息,包括队列ID、队列长度、消息节点大小、头节点、尾节点、可读节点数量、可写节点数量、等待读操作的任务、等待写操作的任务 |
开发流程
- 用LOS_QueueCreate创建队列。创建成功后,可以得到队列ID。
- 通过LOS_QueueWrite或者LOS_QueueWriteCopy写队列。
- 通过LOS_QueueRead或者LOS_QueueReadCopy读队列。
- 通过LOS_QueueInfoGet获取队列信息。
- 通过LOS_QueueDelete删除队列。
说明:
系统支持的最大队列数是指:整个系统的队列资源总个数,而非用户能使用的个数。例如:系统软件定时器多占用一个队列资源,那么用户能使用的队列资源就会减少一个。
创建队列时传入的队列名和flags暂时未使用,作为以后的预留参数。
队列接口函数中的入参timeOut是相对时间。
LOS_QueueReadCopy和LOS_QueueWriteCopy及LOS_QueueWriteHeadCopy是一组接口,LOS_QueueRead和LOS_QueueWrite及LOS_QueueWriteHead是一组接口,每组接口需要配套使用。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,用户必须保证调用LOS_QueueRead获取到的指针所指向的内存区域在读队列期间没有被异常修改或释放,否则可能导致不可预知的后果。
LOS_QueueRead和LOS_QueueReadCopy接口的读取长度如果小于消息实际长度,消息将被截断。
鉴于LOS_QueueWrite和LOS_QueueWriteHead和LOS_QueueRead这组接口实际操作的是数据地址,也就意味着实际写和读的消息长度仅仅是一个指针数据,因此用户使用这组接口之前,需确保创建队列时的消息节点大小,为一个指针的长度,避免不必要的浪费和读取失败。
编程实例
实例描述
创建一个队列,两个任务。任务1调用写队列接口发送消息,任务2通过读队列接口接收消息。
- 通过LOS_TaskCreate创建任务1和任务2。
- 通过LOS_QueueCreate创建一个消息队列。
- 在任务1 SendEntry中发送消息。
- 在任务2 RecvEntry中接收消息。
- 通过LOS_QueueDelete删除队列。
编程示例
本演示代码在./kernel/liteos_a/testsuites/kernel/src/osTest.c中编译验证,在TestTaskEntry中调用验证入口函数ExampleQueue,
为方便用户观察,建议调用ExampleQueue前先调用 LOS_Msleep(5000) 进行短时间延时,避免其他打印过多。
示例代码如下:
#include "los_task.h"
#include "los_queue.h"
static UINT32 g_queue;
#define BUFFER_LEN 50
VOID SendEntry(VOID)
{
UINT32 ret = 0;
CHAR abuf[] = "test message";
UINT32 len = sizeof(abuf);
ret = LOS_QueueWriteCopy(g_queue, abuf, len, 0);
if(ret != LOS_OK) {
dprintf("send message failure, error: %x\n", ret);
}
}
VOID RecvEntry(VOID)
{
UINT32 ret = 0;
CHAR readBuf[BUFFER_LEN] = {0};
UINT32 readLen = BUFFER_LEN;
LOS_Msleep(1000);
ret = LOS_QueueReadCopy(g_queue, readBuf, &readLen, 0);
if(ret != LOS_OK) {
dprintf("recv message failure, error: %x\n", ret);
}
dprintf("recv message: %s\n", readBuf);
ret = LOS_QueueDelete(g_queue);
if(ret != LOS_OK) {
dprintf("delete the queue failure, error: %x\n", ret);
}
dprintf("delete the queue success!\n");
}
UINT32 ExampleQueue(VOID)
{
dprintf("start queue example\n");
UINT32 ret = 0;
UINT32 task1, task2;
TSK_INIT_PARAM_S initParam = {0};
ret = LOS_QueueCreate("queue", 5, &g_queue, 0, 50);
if(ret != LOS_OK) {
dprintf("create queue failure, error: %x\n", ret);
}
dprintf("create the queue success!\n");
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)SendEntry;
initParam.usTaskPrio = 9;
initParam.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
initParam.pcName = "SendQueue";
LOS_TaskLock();
ret = LOS_TaskCreate(&task1, &initParam);
if(ret != LOS_OK) {
dprintf("create task1 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
initParam.pcName = "RecvQueue";
initParam.pfnTaskEntry = (TSK_ENTRY_FUNC)RecvEntry;
ret = LOS_TaskCreate(&task2, &initParam);
if(ret != LOS_OK) {
dprintf("create task2 failed, error: %x\n", ret);
LOS_QueueDelete(g_queue);
return ret;
}
LOS_TaskUnlock();
LOS_Msleep(5000);
return ret;
}
结果验证
编译运行得到的结果为:
start queue example
create the queue success!
recv message: test message
delete the queue success!