鸿蒙轻内核M核源码分析系列十三 消息队列Queue 原创 精华
鸿蒙轻内核M核源码分析系列十三 消息队列Queue
队列(Queue
)是一种常用于任务间通信的数据结构。任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。如果将读队列和写队列的超时时间设置为0,则不会挂起任务,接口会直接返回,这就是非阻塞模式。消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用。
本文通过分析鸿蒙轻内核队列模块的源码,掌握队列使用上的差异。本文中所涉及的源码,以OpenHarmony LiteOS-M
内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_m 获取。
接下来,我们看下队列的结构体,队列初始化,队列常用操作的源代码。
1、队列结构体定义和常用宏定义
1.1 队列结构体定义
在文件kernel\include\los_queue.h
中定义队列控制块结构体为LosQueueCB
,结构体源代码如下。队列状态.queueState
取值OS_QUEUE_UNUSED
、OS_QUEUE_INUSED
,其他结构体成员见注释部分。
1.2 队列常用宏定义
系统支持创建多少队列是根据开发板情况使用宏LOSCFG_BASE_IPC_QUEUE_LIMIT
定义的,每一个队列queueID
是queueID
类型的,取值为[0,LOSCFG_BASE_IPC_QUEUE_LIMIT)
,表示队列池中各个队列的编号。
⑴处的宏从队列池中获取指定队列编号QueueID
对应的队列控制块。⑵处根据双向链表节点readWriteList[OS_QUEUE_WRITE]
获取队列控制块内存地址。
另外,队列中还提供了比较重要的队列读取消息操作相关的枚举和宏。枚举QueueReadWrite
区分队列的读和写,枚举QueueHeadTail
区分队列的首和尾,枚举QueuePointOrNot
区分读写消息时是使用值还是指针。
队列的操作类型使用3比特位的数字来表示,见宏OS_QUEUE_OPERATE_TYPE
的定义,其中高1位表示读写数值还是读写指针地址,中1位表示队首还是队尾,低1位表示读取还是写入。枚举和宏的定义如下:
2、队列初始化
队列在内核中默认开启,用户可以通过宏LOSCFG_BASE_IPC_QUEUE
进行关闭。开启队列的情况下,在系统启动时,在kernel\src\los_init.c
中调用OsQueueInit()
进行队列模块初始化。下面,我们分析下队列初始化的代码。
⑴为队列申请内存,如果申请失败,则返回错误。⑵初始化双向循环链表g_freeQueueList
,维护未使用的队列。⑶循环每一个队列进行初始化,为每一个队列节点指定索引queueID
,并把队列节点插入未使用队列双向链表g_freeQueueList
。代码上可以看出,挂在未使用队列双向链表上的节点是每个队列控制块的写阻塞任务链表节点.readWriteList[OS_QUEUE_WRITE]
。
3、队列常用操作
3.1 队列创建
创建队列函数是LOS_QueueCreate()
,先看看该函数的参数:queueName
是队列名称,实际上并没有使用。len
是队列中消息的数量,queueID
是队列编号,flags
保留未使用。maxMsgSize
是队列中每条消息的最大大小。
我们分析下创建队列的代码。⑴处对参数进行校验,队列编码不能为空,队列消息长度不能太大,队列消息数量和队列消息大小不能为0。⑵处计算消息的实际最大大小msgSize
,即maxMsgSize + sizeof(UINT32)
消息最大大小再加4个字节,在消息的最后4个字节用来保存消息的实际长度。然后调用⑶处函数LOS_MemAlloc()
为对队列动态申请内存,如果内存申请失败,则返回错误码。
⑷处判断g_freeQueueList
是否为空,如果没有可以使用的队列,释放前文申请的内存。⑸处如果g_freeQueueList
不为空,则获取第一个可用的队列节点,接着从双向链表g_freeQueueList
中删除,然后调用宏GET_QUEUE_LIST
获取LosQueueCB *queueCB
,初始化创建的队列信息,包含队列的长度.queueLen
、消息大小.queueSize
,队列内存空间.queue
,消息状态.queueState
,可读的数量.readWriteableCnt[OS_QUEUE_READ]
为0,可写的数量readWriteableCnt[OS_QUEUE_WRITE]
为队列消息长度len
,队列头位置.queueHead
和尾位置.queueTail
为0。
⑹初始化双向链表.readWriteList[OS_QUEUE_READ]
,阻塞在这个队列上的读消息任务会挂在这个链表上。初始化双向链表.readWriteList[OS_QUEUE_WRITE]
,阻塞在这个队列上的写消息任务会挂在这个链表上。初始化双向链表.memList
。⑺赋值给输出参数*queueID
,后续程序使用这个队列编号对队列进行其他操作。
3.2 队列删除
我们可以使用函数LOS_QueueDelete(UINT32 queueID)
来删除队列,下面通过分析源码看看如何删除队列的。
⑴处判断队列queueID
是否超过LOSCFG_BASE_IPC_QUEUE_LIMIT
,如果超过则返回错误码。如果队列编号没有问题,获取队列控制块LosQueueCB *queueCB
。⑵处判断要删除的队列处于未使用状态,则跳转到错误标签QUEUE_END
进行处理。⑶如果队列的阻塞读、阻塞写任务列表不为空,或内存节点链表不为空,则不允许删除,跳转到错误标签进行处理。⑷处检验队列的可读、可写数量是否出错。
⑸处使用指针UINT8 *queue
保存队列的内存空间,⑹处把.queue
置空,把.queueState
设置为未使用OS_QUEUE_UNUSED
,并把队列节点插入未使用队列双向链表g_freeQueueList
。接下来会需要调用⑺处函数LOS_MemFree()
释放队列内存空间。
下面就来看看队列的读写,有2点需要注意:
- 队首、队尾的读写
只支持队首读取,不能队尾读取,否则就不算队列了。除了正常的队尾写消息外,还提供插队机制,支持从队首写入。
- 队列消息数据内容
往队列中写入的消息的类型有2种,即支持按地址写入和按值写入(带拷贝)。按哪种类型写入,就需要配对的按相应的类型去读取。
队列读取接口的类别,归纳如下:
读写接口类别 | 接口名称 | 描述 |
---|---|---|
读队列/队尾写队列 | LOS_QueueRead、 LOS_QueueWrite | 从指定队列头节点读、往队列尾节点写入。队列消息数据为内存地址,传引用 |
读队列/队尾写队列,带拷贝 | LOS_QueueReadCopy、LOS_QueueWriteCopy | 从指定队列头节点读、往队列尾节点写入。队列消息数据为数据值,传数值 |
读队列/队首写队列 | LOS_QueueRead、LOS_QueueWriteHead | 从指定队列头节点读、往队列头节点写入。队列消息数据为内存地址,传引用 |
读队列/队首写队列,带拷贝 | LOS_QueueReadCopy、LOS_QueueWriteHeadCopy | 从指定队列头节点读、往队列头节点写入。队列消息数据为数据值,传数值 |
3.3 队列读取
我们知道有2个队列读取方法,按指针地址读取的函数LOS_QueueRead()
和按消息数值读取的函数LOS_QueueReadCopy()
。我们先看下函数LOS_QueueRead()
,该函数的参数有4个,队列编号queueID
,存放读取到的消息的缓冲区地址*bufferAddr
,存放读取到的消息的缓冲区大小bufferSize
,读队列消息的等待超时时间timeOut
。代码如下,我们分析下代码。
⑴处校验传入参数,队列编号不能超出限制,传入的指针不能为空,缓冲大小不能为0。如果timeout
不为零,不能在中断中读取队列。⑵处操作类型表示队首读取消息指针,然后调用函数OsQueueOperate()
进一步操作队列。
我们进一步分析下函数OsQueueOperate()
,这是是比较通用的封装,读取,写入都会调用这个函数,我们以读取队列为例分析这个函数。⑴处获取队列的操作类型,为读取操作。⑵处先调用函数OsQueueOperateParamCheck()
进行参数校验,校验队列是使用中的队列,并对读写消息大小进行校验。⑶处如果可读数量为0,无法读取时,如果是零等待则返回错误码。如果当前锁任务调度,跳出函数执行。否则,执行⑷把当前任务放入队列的读取消息阻塞队列,然后触发任务调度,后续的代码暂时不再执行。如果可读的数量不为0,可以继续读取时,执行⑹处代码把可读数量减1,然后继续执行⑺处代码读取队列。
等读取队列阻塞超时,或者队列可以读取后,继续执行⑸处的代码。如果是发生超时,队列还不能读取,更改任务状态,跳出函数执行。如果队列可以读取了,继续执行⑺处代码读取队列。⑻处在成功读取队列后,如果有任务阻塞在写入队列,则获取阻塞链表中的第一个任务resumedTask
,然后调用唤醒函数OsSchedTaskWake()
把待恢复的任务放入就绪队列,触发一次任务调度。如果无阻塞任务,则把可写入的数量加1。
我们再继续看下函数OsQueueBufferOperate()
是具体如何读取队列的。⑴处switch-case
语句根据操作类型获取操作位置。对于⑵头部读取的情况,先获取读取位置queuePosition
。然后,如果当前头节点位置.queueHead
加1等于队列消息长度,头节点位置.queueHead
设置为0,否则加1。对于⑶头部写入的情况,如果当前头节点位置.queueHead
等于0,头节点位置.queueHead
设置为队列消息长度减1即queueCB->queueLen - 1
,否则头节点位置.queueHead
减1即可。然后,获取要写入的位置queuePosition
。对于⑷尾部写入的情况,先获取写入位置queuePosition
。然后,如果当前尾节点位置.queueTail
加1等于队列消息长度,尾节点位置.queueTail
设置为0,否则加1。
⑸处基于获取的队列读取位置获取队列消息节点queueNode
。⑹处判断操作类型如果是按指针读写消息,直接读取消息节点的数据写入指针对应的缓冲区*(UINT32 *)bufferAddr
,或直接把指针对应的缓冲区*(UINT32 *)bufferAddr
数据写入消息节点即可。我们接着看如何按数数据读写消息,⑺处代码用于读取数据消息。每个消息节点的后4个字节保存的是消息的长度,首先获取消息的长度msgDataSize
,然后把消息内容读取到bufferAddr
。再看看⑻处如何写入队列消息,首先把消息内容写入到queueNode
,然后再把消息长度的内容写入到queueNode + queueCB->queueSize - sizeof(UINT32)
,就是每个消息节点的后4字节。
3.4 队列写入
我们知道,有4个队列写入方法,2个队尾写入,2个队首写入,分别包含按指针地址写入消息和按数值写入消息。LOS_QueueWrite()
会调用LOS_QueueWriteCopy()
,LOS_QueueWriteHead()
会调用LOS_QueueWriteHeadCopy()
,然后指定不同的操作类型后,会进一步调用前文已经分析过的函数OsQueueOperate()
。
小结
本文带领大家一起剖析了鸿蒙轻内核的队列模块的源代码,包含队列的结构体、队列池初始化、队列创建删除、读写消息等。感谢阅读,如有任何问题、建议,都可以留言给我们: https://gitee.com/openharmony/kernel_liteos_m/issues 。为了更容易找到鸿蒙轻内核代码仓,建议访问 https://gitee.com/openharmony/kernel_liteos_m ,关注Watch
、点赞Star
、并Fork
到自己账户下,谢谢。
队列在任何环境中都是很重要的。