2021SC@SDUSC
一. 队列是什么
队列(queue)是一种遵循「先入先出」(FIFO)的规则并且只能在一端插入元素、在另一端删除元素的数据结构。TencentOS tiny暂时不支持后进先出原则LIFO操作队列,但是支持后进先出LIFO操作消息队列。
首先需要说明的是,一般我们认为队列即为消息队列,但是在TencentOS tiny中是将这两个概念分开的,消息队列是队列的底层。
队列是一种常用于进程间通信的数据结构,队列可以在进程与进程间、中断和进程间传递消息,实现了进程能够接收来自其他进程或中断的不固定长度的消息。
进程能够从队列里面读取消息,当队列中的消息是空时,读取消息的进程将被阻塞。进程阻塞的方式会有三种,下面我们会说到,其中一种比较常用的就是用户可以指定进程等待消息的时间timeout,在这段时间中,如果队列为空,该进程将保持阻塞状态以等待队列数据有效。当队列中有新消息时,被阻塞的进程会被唤醒并处理新消息;当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,进程也会自动从阻塞态转为就绪态。
(一)异步通信方式
队列就是一种异步的通信方式。那么什么是异步的通信方式?异步通信中,接收方不知道数据(消息)什么时候会到达(队列中),收发双发有各自自己的时钟,发送方发送的时间间隔可以不均,接收方是在数据的起始位和停止位的帮助下实现信息同步的。
(二)队列的实现
队列通过两个指针来实现入队和出队操作:
- 队头指针:永远指向此队列的第一个数据元素;
- 队尾指针:永远指向此队列的最后一个数据元素;
那这两个指针是如何实现入队、出队两种操作的呢?
- 入队:将一个元素添加到队尾,并将队尾指针+1后移
- 出队:将从队头中取出一个元素,并将队头指针+1后移
(三)队列的存储
队列的数据存储方式有两种:
- 基于静态连续内存(数组)存储
- 基于动态内存(链表节点)存储
二. TencentOS-tiny中两种特殊队列的实现
(一)环形队列
TencentOS-tiny中环形队列的实现在tos_ring_queue.h
和tos_ring_queue.c
中,代码及注释如下:
(实现队列的结构体定义)
typedef struct k_ring_queue_st {
knl_obj_t knl_obj;
uint16_t head; //队头指针
uint16_t tail; //队尾指针
size_t total; //记录队列中元素的个数
uint8_t *pool; //队列底层的存储结构(一个数组)
size_t item_size; //队列中每个元素的大小,单位:字节
size_t item_cnt; //队列中可以容纳的元素数量
} k_ring_q_t;
(创建并初始化环形队列,将队头指针和队尾置0)
__API__ k_err_t tos_ring_q_create(k_ring_q_t *ring_q, void *pool, size_t item_cnt, size_t item_size)
{
//省略了参数合法性检查代码
ring_q->head = 0u;
ring_q->tail = 0u;
ring_q->total = 0;
ring_q->pool = (uint8_t *)pool;
ring_q->item_size = item_size;
ring_q->item_cnt = item_cnt;
return K_ERR_NONE;
}
(判断环形队列是否为满或者是否为空)
__API__ int tos_ring_q_is_empty(k_ring_q_t *ring_q)
{
TOS_CPU_CPSR_ALLOC();
int is_empty = K_FALSE;
//省略了参数合法性检查代码
TOS_CPU_INT_DISABLE();
is_empty = (ring_q->total == 0 ? K_TRUE : K_FALSE);
TOS_CPU_INT_ENABLE();
return is_empty;
}
__API__ int tos_ring_q_is_full(k_ring_q_t *ring_q)
{
TOS_CPU_CPSR_ALLOC();
int is_full = K_FALSE;
//省略了参数合法性检查代码
TOS_CPU_INT_DISABLE();
is_full = (ring_q->total == ring_q->item_cnt ? K_TRUE : K_FALSE);
TOS_CPU_INT_ENABLE();
return is_full;
}
(环形队列入队操作)
__API__ k_err_t tos_ring_q_enqueue(k_ring_q_t *ring_q, void *item, size_t item_size);
__STATIC_INLINE__ void ring_q_item_increase(k_ring_q_t *ring_q)
{
ring_q->tail = RING_NEXT(ring_q, ring_q->tail);
++ring_q->total;
}
(环形队列出队操作)
__API__ k_err_t tos_ring_q_dequeue(k_ring_q_t *ring_q, void *item, size_t *item_size);
__STATIC_INLINE__ void ring_q_item_decrease(k_ring_q_t *ring_q)
{
ring_q->head = RING_NEXT(ring_q, ring_q->head);
--ring_q->total;
}
在入队和出队操作的时候都使用了 RING_NEXT 这个宏
作用:用来获取在环形队列中的下一个位置:
#define RING_NEXT(ring_q, index) ((index + 1) % ring_q->item_cnt)
(二)优先级队列
优先级队列也是一种基于队列的数据结构,但是它**「不遵循FIFO」,而是按照每个元素的优先级进行出队:「最高优先级的先出队」**。
TencentOS-tiny中环形队列的实现在tos_prio_queue.h
和tos_prio_queue.c
中。
优先级队列在数据入队的时候,会按照入队元素的优先级进行一次排序,「将优先级值最小(优先级最高的元素)放在队头」,出队的时候只需要取第一个元素即可。
正是因为这种特性(入队前排序),优先级队列的底层存储结构不能使用数组(排序太麻烦),而是使用了二项堆的数据结构。
二项堆是什么?
二项堆是二项树的集合或是由一组二项树组成。二项堆具有良好的性质。在 O ( l o g 2 n ) O(log_2n) O(log2n)的时间内即可完成两个二项堆合并操作,所以二项堆是可合并堆,而仅仅需要 O ( 1 ) O(1) O(1)的时间,二项堆即可完成插入操作。因此,基于二项堆实现的优先队列和进程调度算法有着很好的时间性能。同时由于二项树的结构特性和性质,二项树在网络优化等诸多领域也应用广泛。
总之,二项堆的优势就是可以快速合并两个堆,快速合并两个优先级队列。
源码分析:
(创建优先级队列)
__API__ k_err_t tos_prio_q_create(k_prio_q_t *prio_q, void *mgr_array, void *pool, size_t item_cnt, size_t item_size);
参数 | 描述 |
---|---|
prio_q | 优先级队列控制块指针 |
mgr_array | 提供一块缓冲区用于内部管理 |
pool | 队列的缓冲区 |
item_cnt | 队列可容纳的元素数量 |
item_size | 每个元素的大小,单位字节 |
(优先级队列入队操作)
__API__ k_err_t tos_prio_q_enqueue(k_prio_q_t *prio_q, void *item, size_t item_size, k_prio_t prio);
遵循数值越小,优先级越高
(优先级队列出队操作)
__API__ k_err_t tos_prio_q_dequeue(k_prio_q_t *prio_q, void *item, size_t *item_size, k_prio_t *prio);
prio需要传入一个地址,用于记录出队元素的优先级
三. 总结
① 普通队列是一种只能在一端入队,在一端出队的数据结构,规则:FIFO。
② 环形队列对内存空间的利用率最高,使用最多,规则:FIFO。
③ 优先级队列不遵循FIFO,每个元素都有自己的优先级,规则:优先级最高的元素先出队。