2021SC@SDUSC
文章目录
一. TencentOS-tiny中实现队列的数据结构
这部分主要讲一下TencentOS-tiny中实现队列的一些数据结构及其他相关知识
(一)队列控制块
TencentOS tiny 通过队列控制块操作队列,在TencentOS tiny中的数据类型为k_queue_t,队列控制块由多个元素组成,主要有 pend_obj_t 类型的pend_obj以及k_msg_queue_t类型的msg_queue消息列表。
其实整个队列的实现非常简单,主要靠msg_queue中的queue_head成员变量(这其实是一个消息列表(消息链表)),所有的消息都会被记录在这个消息列表中,当读取消息的时候,会从消息列表读取消息。下面看一下源码中的这些变量是如何定义的:
下面这个是继承自内核对象的数据结构(\kernel\core\include\tos_pend.h 的 35 行)
typedef struct pend_object_st {
pend_type_t type;
k_list_t list;
} pend_obj_t;
下面这个是消息列表的数据类型(消息队列控制块)(在 \kernel\core\include\tos_msg.h 文件的第 13 行)
typedef struct k_msg_queue_st {
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
knl_obj_t knl_obj;
#endif
k_list_t queue_head;
} k_msg_queue_t;
下面这个就是我们说的队列控制块的代码实现了( \kernel\core\include\tos_queue.h 文件的第 6 行)
typedef struct k_queue_st {
pend_obj_t pend_obj;
k_msg_queue_t msg_queue;
} k_queue_t;
(二)消息控制块
除了队列控制块外,还有消息控制块,这是因为TencentOS tiny中实现队列是依赖消息队列的,既然队列可以传递数据(消息),则必须存在一种可以存储消息的数据结构,即消息控制块。
消息控制块中记录了消息的存储地址msg_addr,以及消息的大小msg_size,此外还存在一个list成员变量,可以将消息挂载到队列的消息列表中。下面来看源码实现:
消息控制块数据结构(\kernel\core\include\tos_msg.h 文件的第 7 行)
typedef struct k_message_st {
k_list_t list;
void *msg_addr;
size_t msg_size;
} k_msg_t;
(三)进程控制块中的消息成员变量
假设进程A正在队列中等待消息,此时中断或其他进程往进程A等待的队列写入(发送)一个消息,那么这个消息不会被挂载到队列的消息列表中(为提升效率而跳过这个步骤),而是会直接被记录在进程A的进程控制块中,表示进程A从队列中等到了这个消息,因此进程控制块必须也存在一些成员变量用于记录消息相关信息(如消息地址、消息大小等):
进程控制块数据结构(\kernel\core\include\tos_task.h文件的第 90 行)
typedef struct k_task_st {
···
#if TOS_CFG_MSG_EN > 0u
void *msg_addr; /**< 保存接收到的消息地址 */
size_t msg_size; /**< 保存接收到的消息大小 */
#endif
···
} k_task_t;
(四)与消息相关的宏定义
#define TOS_CFG_QUEUE_EN 1u
#define TOS_CFG_MSG_EN 1u
#define TOS_CFG_MSG_POOL_SIZE 3u
tos_config.h
文件中,队列组件的宏定义TOS_CFG_QUEUE_EN
,消息队列组件宏定义TOS_CFG_MSG_EN
,系统支持的消息池中消息的个数宏定义TOS_CFG_MSG_POOL_SIZE
。
(五)消息池
在TencentOS tiny中定义了一个数组**k_msg_pool[TOS_CFG_MSG_POOL_SIZE]**作为消息池,它的数据类型是消息控制块类型k_msg_t,因为在使用消息队列的时候存取消息比较频繁,而在系统初始化的时候就将这个大数组的各个元素串初始化,并挂载到空闲消息列表中k_msg_freelist,组成我们说的消息池k_msg_pool,而池中的成员变量就是我们所说的消息。
为什么用池化的方式处理信息?
因为高效,复用率高,就像我们在池塘中去一勺水,在使用完毕再将其归还到池塘,这种操作是非常高效的,并且在有限资源的嵌入式中能将资源重复有效地利用起来。
消息池相关的定义(\kernel\core\tos_global.c文件 第 51 行)
#if TOS_CFG_MSG_EN > 0u
TOS_LIST_DEFINE(k_msg_freelist);
k_msg_t k_msg_pool[TOS_CFG_MSG_POOL_SIZE];
#endif
二. 队列的创建
前面在讲环形队列和优先级队列时,已经提到过这两种队列的创建,现在再说一下最基本的这种队列的创建,下面来看创建的源码:
(kernel\core\tos_queue.c 第 5 行)
__API__ k_err_t tos_queue_create(k_queue_t *queue)
{
TOS_PTR_SANITY_CHECK(queue);
pend_object_init(&queue->pend_obj, PEND_TYPE_QUEUE);
tos_msg_queue_create(&queue->msg_queue);
return K_ERR_NONE;
}
正如源码中我们见到的:tos_queue_create()函数用于创建一个队列。
每创建一个新的队列都需要为其分配RAM,在创建的时候我们需要自己定义一个队列控制块,其内存是由编译器自动分配的。
在创建的过程中实际上就是将队列控制块的内容进行初始化,将队列控制块的 pend_obj成员变量中的type 属性标识为PEND_TYPE_QUEUE,标识这是一个队列,然后调用消息队列中的API函数tos_msg_queue_create()将队列的消息成员变量msg_queue初始化,实际上就是初始化消息列表。
三. 队列清空
清空队列实际上就是将消息释放回消息池中,本质上还是调用tos_msg_queue_flush()
函数。我们可以直接看tos_msg_queue_flush()
函数的实现:
(\kernel\core\tos_queue.c 第 41 行)
__API__ k_err_t tos_queue_flush(k_queue_t *queue)
{
TOS_CPU_CPSR_ALLOC();
TOS_PTR_SANITY_CHECK(queue);
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
if (!pend_object_verify(&queue->pend_obj, PEND_TYPE_QUEUE)) {
return K_ERR_OBJ_INVALID;
}
#endif
TOS_CPU_INT_DISABLE();
tos_msg_queue_flush(&queue->msg_queue);
TOS_CPU_INT_ENABLE();
return K_ERR_NONE;
}
四. 队列销毁
在这之前我没提到过队列的销毁,现在来补充一下队列的销毁。
但其实队列销毁的过程中是内含刚才说的队列清空步骤的,先来看一下队列销毁的源码,我们通过分析源码来理解队列销毁的过程:
(\kernel\core\tos_queue.c 第 14 行)
__API__ k_err_t tos_queue_destroy(k_queue_t *queue)
{
TOS_CPU_CPSR_ALLOC();
TOS_PTR_SANITY_CHECK(queue);
#if TOS_CFG_OBJECT_VERIFY_EN > 0u
if (!pend_object_verify(&queue->pend_obj, PEND_TYPE_QUEUE)) {
return K_ERR_OBJ_INVALID;
}
#endif
TOS_CPU_INT_DISABLE();
if (!pend_is_nopending(&queue->pend_obj)) {
pend_wakeup_all(&queue->pend_obj, PEND_STATE_DESTROY);
}
pend_object_deinit(&queue->pend_obj);
tos_msg_queue_flush(&queue->msg_queue);
TOS_CPU_INT_ENABLE();
knl_sched();
return K_ERR_NONE;
}
tos_queue_destroy()函数用于销毁一个队列,当队列不在使用是可以将其销毁,销毁的本质其实是将队列控制块的内容进行清除,首先判断一下队列控制块的类型是PEND_TYPE_QUEUE,这个函数只能销毁队列类型的控制块。
然后判断是否有进程在等待队列中的消息,如果有则调用pend_wakeup_all()函数将这项进程唤醒,然后调用tos_msg_queue_flush()函数将队列的消息列表的消息全部“清空”,“清空”的意思是将挂载到队列上的消息释放回消息池(如果队列的消息列表存在消息,使用msgpool_free()函数释放消息)
knl_object_deinit()函数是为了确保队列已经被销毁,此时队列控制块的pend_obj成员变量中的type 属性标识为KNL_OBJ_TYPE_NONE。最后在销毁队列后进行一次进程调度,以切换进程(刚刚可能唤醒了进程)。
需要注意的是:因为队列控制块的RAM是由编译器静态分配的,所以即使是销毁了队列,这个内存也是没办法释放的
本周的内容就到此为止了,下周我们将以如何从队列中读消息和往队列里写消息为开始来分析…