参考:【野火】物联网操作系统 LiteOS 开发实战指南
3 LiteOS消息队列
3.1 消息队列简介
- 消息队列是一种常用于任务间通信的数据结构
- 可以在任务与任务间、中断和任务间传递消息,实现接收来自任务或者中断的不固定长度的消息,并根据不同的接口选择传递消息是否存放在自己的空间
- 消息队列是一种异步的通信方式,用户在处理业务时,消息队列提供异步处理机制,允许将一个消息放入队列,但并不立即处理它
- 消息队列使用时需要包含头文件
los_queue.h
3.2 LiteOS消息队列的特点
- 消息以先进先出方式排队,支持异步读写工作方式
- 读队列和写队列都支持超时机制
- 发送消息类型由通信双方约定,可以允许不同长度(不超过队列节点最大值)的任意类型消息
- 消息支持先进后出的方式排队,往队首发送消息(LIFO)
- 一个任务能够从任意一个消息队列接收和发送消息
- 多个任务能够从同一个消息队列接收和发送消息
- 当队列使用结束后,如果是动态申请的内存,需要通过释放内存函数回收
3.3 LiteOs消息队列的运作机制
- 创建队列时,用户根据传入的队列长度和消息节点来开辟相应的内存空间
- 队列控制块中维护消息头结点位置
usQueueHead
和一个消息尾节点位置usQueueTail
来表示消息队列的消息存储情况 - 可以将LiteOs消息队列看成一个_环形队列_
- 写队列是从
usQueueTail
所指的尾部的空闲节点写入区域,利用usReadWriteableCnt[OS_QUEUE_WRITE]
来判断是否可以写入 - 读队列是从
usQueueHead
找到最先入队列的消息节点进行读取,利用usReadWriteableCnt[OS_QUEUE_READ]
判断队列是否有消息可读取,若没有消息的队列进行读队列操作会引起任务挂起 - 删除队列根据传入的队列
ID
寻找到对应的队列,把队列状态设置为未使用,释放原队列所占空间 - LiteOs的消息队列采用两个双向链表来维护一个链表指向消息队列的头部,一个链表指向消息队列的尾尾
(stReadWriteList[QUEUE_HEAD_TAIL])
,通过访问这两个链表就能直接访问对应的消息空间,并且通过消息队列控制块 中的读写类型(usReadWriteableCnt[QUEUE_READ_WRITE])
来操作消息队列 - 消息队列的运作过程如下图所示
LiteOs消息队列运作图
消息队列读写消息图
3.4 LiteOs消息队列的传输机制
-
消息队列是一种FIFO线性表,也支持LIFO
-
消息数据传输支持值传递(拷贝)和引用传递
-
两种传递方式比较
消息传递方式 大小 重要性 拷贝 数据量小的场合 数据重要性高 引用方式 数据量大的场合 重要性一般场合
3.5 消息队列的阻塞机制
- 顾名思义,就是指读/写消息队列时,能够完整的不受其他任务干扰的完成读/写,即阻塞机制
3.5.1 出队阻塞
- 读操作,若消息队列中没有消息,会有三种选择
- 该任务继续干别的事情,不会进入阻塞状态
- 该任务等待消息队列的消息,等待时间由用户定制,等待过程即为阻塞状态,消息队列有了对应的消息后,该任务转为就绪状态,然后根据该任务的优先级读取队列消息,若超时,则该任务放弃等待,去干别的事情
- 该任务死等,等不到消息就不干别的事情,该任务一直进入阻塞状态,直到完成读取队列的消息
3.5.2 入队阻塞
- 写操作,若消息队列已经满了,会进行如下处理
- 发送消息操作的时候,当且仅当队列被允许入队时,发送者才能成功发送消息
- 队列中无可用消息空间时,系统会根据用户指定的阻塞超时时间将任务阻塞,在指定的超时时间内如果还不能完成入队操作,发送消息的任务或者中断服务程序会收到一个错误代码
LOS_ERRNO_QUEUE_ISFULL
,然后解除阻塞状态 - 只有在任务中发送消息才允许进行阻塞状态,在中断中发送消息不允许带有阻塞机制的,否则返回错误代码
LOS_ERRNO_QUEUE_READ_IN_INTERRUPT
- 如果有多个任务阻塞在一个消息队列中,那么这些阻塞的任务将按照任务优先级进行排序,优先级高的任务将优先获得队列的访问权
3.6 常用消息队列的函数介绍
3.6.1 使用队列模块的典型流程
- 创建消息队列
LOS_QueueCreate
,在创建任务之前 - 创建成功后,可以得到消息队列的
ID
值 - 写队列操作函数
LOS_QueueWrite
,在对应需要传递消息的任务中使用 - 读队列操作函数
LOS_QueueRead
,在对应需要读取消息的任务中使用 - 删除队列
LOS_QueueDelete
3.6.2 创建消息队列LOS_QueueCreate()
-
创建消息队列的内存空间:
内存空间=消息队列控制块大小+(单个消息空间大小+4字节)∗消息队列长度 内存空间=消息队列控制块大小+(单个消息空间大小+4字节)*消息队列长度 内存空间=消息队列控制块大小+(单个消息空间大小+4字节)∗消息队列长度 -
当消息队列被创建时,系统会为控制块分配对应的内存空间,用于保存消息队列的(消息存储位置,头指针,尾指针,消息大小,以及队列长度等)
-
UINT32 LOS_QueueCreate(CHAR *pcQueueName,//消息队列的名称,暂时未使用 UINT16 usLen,// 队列长度 UINT32 *puwQueueID,//成功创建的队列控制结构ID,需要用户在创建前定义 UINT32 uwFlags,//队列参数,保留参数,暂时不使用 UINT16 usMaxMsgSize )//最大消息字节 { ... } // 队列控制块 typedef struct tagQueueCB { UINT8 *pucQueue; /**< 队列指针 */ UINT16 usQueueState; /**< 队列状态 */ UINT16 usQueueLen; /**< 队列中消息个数 */ UINT16 usQueueSize; /**< 消息节点大小 */ UINT16 usQueueID; /**< 队列ID */ UINT16 usQueueHead; /**< 消息头结点位置 */ UINT16 usQueueTail; /**< 消息尾结点位置 */ UINT16 usReadWriteableCnt[2]; /**< 可读或者可写资源的计数0:可读,1:可写 */ LOS_DL_LIST stReadWriteList[2]; /**< 指向要读取或写入的链表的指针0: 读列表 */ LOS_DL_LIST stMemList; /** 指向内存链表的指针 */ } QUEUE_CB_S;
-
先定义队列
ID
,再调用LOS_QueueCreate()
函数创建,才能成功创建消息队列
3.6.2 消息队列删除函数LOS_QueueDelete()
-
队列删除函数是直接根据队列ID直接删除的
UINT32 LOS_QueueDelete(UINT32 uwQueueID)
-
队列在使用或者阻塞中是不能被删除的
3.6.3 消息队列写数据函数
-
任务或者中断程序都可以给消息队列写入消息
-
当写入消息时,
- 如果队列未满,LiteOS会将消息拷贝到消息队列的尾部,
- 若满,会根据用户指定的阻塞超时时间进行阻塞,在这段时间中,如果队列还是满的,该任务将保持阻塞状态以等待有空闲的消息空间,如果其他任务从其等待的队列中读取了数据,则消息队列空出空间,该等待的任务自动由阻塞状态转为就绪态,并写入消息
- 当任务等待的时间超过指定的阻塞时间,即使队列中还是满的,任务也会自动从阻塞状态变为就绪态,此时发送消息的任务/中断程序会收到一个错误码:
LOS_ERRNO_QUEUE_ISFULL
-
不带拷贝写入函数
LOS_QueueWrite()
:UINT32 LOS_QueueWrite(UINT32 uwQueueID, // 队列ID VOID *pBufferAddr, // 存储写入的数据的起始地址 UINT32 uwBufferSize, // 存入缓存区的大小 UINT32 uwTimeOut); // 等待时间(0~LOS_WAIT_FOREVER)
-
写入队列需要注意几点:
- 在使用写入队列的操作前应先创建要写入的队列
- 在中断上下文环境中, 必须使用非阻塞模式写入,也就是等待时间为 0 个 tick
- 在初始化 LiteOS 之前无法调用此 API
- 将写入由 uwBufferSize 指定大小的数据,其数据存储在 BufferAddr 指定的地址 ,那么该数据地址必须有效,否则会发生错误
- 写入队列节点中的是数据的地址
-
带拷贝写入
LOS_ QueueWriteCopy()
UINT32 LOS_QueueWriteCopy(UINT32 uwQueueID, // 队列ID VOID *pBufferAddr, // 存储写入的数据的起始地址 UINT32 uwBufferSize, // 存入缓存区的大小 UINT32 uwTimeOut); // 等待时间(0~LOS_WAIT_FOREVER)
- 注意点同不带拷贝写入,区别是写入队列中的是存储在
pBufferAddr
中的数据
- 注意点同不带拷贝写入,区别是写入队列中的是存储在
3.6.4 消息队列读数据函数
-
LOS_ QueueRead()
(不带拷贝方式读出)UINT32 LOS_QueueRead(UINT32 uwQueueID, // 队列ID VOID *pBufferAddr, // 存储写入的数据的起始地址 UINT32 uwBufferSize, // 存入缓存区的大小 UINT32 uwTimeOut); // 等待时间(0~LOS_WAIT_FOREVER)
-
注意点:
- 在使用读取队列的操作前应先创建要写入的队列
- 队列读取采用的是先进先出(FIFO)模式, 首先读取首先存储在队列中的数据
- 必须要我们自己定义一个存储读取出来的数据的地方,并且把存储数据的起始地址传递给
LOS_ QueueRead()
函数,否则,将发生地址非法的错误。 - 在中断上下文环境中, 必须使用非阻塞模式写入,也就是等待时间为 0 个 tick
- 在初始化 LiteOS 之前无法调用此 API
pBufferAddr
里存放的是队列节点的地址LOS_QueueReadCopy()
和LOS_QueueWriteCopy()
是一组接口,LOS_QueueRead()
和LOS_QueueWrite()
是一组接口,两组接口需要配套使用
-
LOS_ QueueReadCopy()
(带拷贝读出)UINT32 LOS_QueueReadCopy(UINT32 uwQueueID, // 队列 ID VOID * pBufferAddr, // 存储获取数据的起始地址 UINT32 * puwBufferSize,// 保存读取之后数据大小的值 UINT32 uwTimeOut) // 等待时间
- 注意点同上,不同的是
pBufferAddr
存放的是消息队列中的数据- 需要定义一个空间保存读取数据的大小
- 注意点同上,不同的是