嵌入式实时操作系统6——链表数据结构
Posted liyinuo2017
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌入式实时操作系统6——链表数据结构相关的知识,希望对你有一定的参考价值。
- 链表结构的作用
在《RTOS系列5——就绪表》中描述了操作系统内核中的就绪表使用了链表结构,就绪表的框图如下:
在操作系统内核中不仅仅是就绪表使用了链表结构,等待表和挂起表也都用到了链表结构。
链表数据结构有以下优点:
1、在保留原有物理顺序的情况下,插入和删除速度快,效率高。插入和删除只需要改变几个指针变量。
2、链表中的表项数量没有上限。存储的表项上限只与内存空间大小有关,理论上如果内存无限大,链表中的表项可以动态增加到无限个。
3、动态分配内存,需要用多少个表项,就分配几个表项,不需要预先分配内存,不存在内存浪费的情况。
分析:
1、插入和删除效率高
如果要在元素“3”之后插入一个“8”,就需要移动元素“3”之后的“14179”5个元素。
使用链表结构时,只有改变4个指针的数据既可。如果元素有很多个时,链表的这种操作的将会带来极高的效率。
2、链表中的表项数量没有上限
如果内存无限大,链表中的表项可以动态增加到无限个,不会出现元素满的情况。
3、动态分配内存
分配内存,不需要预先分配内存,不存在内存浪费的情况。
- 链表数据结构
链表分为单向链表和双向链表,这两种链表的结构如下:
双向链表中的每个对象中包含:关键字key和两个指针:next和prev,next是指向下一个后继元素的指针,prev是指向上一个前驱元素的指针,data为用户数据。
单向链表中的每个对象中包含:关键字key和一个指针:next,next是指向下一个后继元素的指针,data为用户数据。
关键字key用于查找对象和对象排序。
链表有多种形式,它可以是排序的,可以是未排序的,可以是循环的,可以是非循环的。
如果链表是排序的,链表的线性顺序和链表元素中的关键字key的线性顺序一致。在循环列表中,表头元素的prev指针指向表尾元素,表尾元素的next指针指向表头元素。
链表的基本操作是:
1、查询一个元素
2、插入一个元素
3、删除一个元素
操作系统内核中通常使用的链表为双向循环链表。双向循环链表的结构如下图:
- 链表实现
链表有两种常见的实现方式:
1、链表中包含用户数据。
2、用户数据中包含链表。
C语言实现如下:
这两种实现的链表的结构框图如下:
链表中包含用户数据的方式最大的问题是:当用户数据改变时,整个链表结构也会改变,不同的用户数据需要定义不同的链表结构。
用户数据中包含链表的方式可以通用一种链表结构,用户数据可以不同,操作系统内核种通常使用用户数据中包含链表的方式。
- 万能药
上文提到用户数据中包含链表的方式可以通用一种链表结构,可以适应不同的用户数据。但是世界上没有万能药,这种链表形式同样存在一个问题:
由于链表指针指向的是另外一个链表对象,此时我们将无法得到整个数据对象的地址。由上图可知我们通过next指针可以得到下一个list元素的地址,但是我们无法获取整个数据对象的地址。
办法总比困难多!我们有两种常用的方法解决“无法获取整个数据对象的地址”的问题:
1、指针法。
2、地址反推法。
指针法
指针法是在链表结构种增加一个void * owner指针,用这个指针指向整个数据对象的地址,因为是void * 类型,owner指针可以指向任意类型的对象。C语言代码实现如下:
这种链表的结构框图如下:
在指针法中,使用链表结构中的owner指针可以定位到整个数据对象的地址。在小型的实时操作系统中通常使用这种方式。
地址反推法
地址反推法是已知数据对象的类型和链表的地址,可以反推得到数据对象的地址。C语言代码实现如下:
container_of是可以根据结构体的成员变量获取所在结构体的首地址,这种方法的原理是:编译器记录了每一个结构体类型数据中的每一个元素的偏移地址,现在已知一个结构体类型,和该结构体内的一个元素地址(链表元素地址),就能反推出该结构体对象的地址。
linux内核常用这种地址反推法定位数据对象的地址。
- 哨兵
用代码演示一下向链表中插入对象:
以上代码在一个演示了在一个空链表中插入了第一个对象。非空链表中插入对象是同样的操作吗?代码如下:
由此可见链表在空状态和非空状态插入和删除对象的操作是不同的,这样就给提高了程序的复杂度,需要在插入和删除对象前判断链表是否为“空”。
哨兵机制(表头机制)可以解决这个问题。哨兵是一个哑对象,作用是简化边界条件处理。使用哨兵可以使得代码紧凑,使用哨兵机制的链表如下如:
使用哨兵机制(表头机制)后链表在空状态和非空状态插入和删除对象的操作是相同的,这样可以使得代码紧凑,清晰。
- 深入分析FREERTOS就绪表
FreeRTOS就绪表的源码如下:
FreeRTOS中的TCB结构如下:
FreeRTOS中的就绪表特点:
1、使用的是用户数据中包含链表的方式。
2、使用指针法定位对象地址。
3、使用哨兵机制(表头机制)。
分析:
1、用户数据中包含链表
FreeRTOS中在TCB数据结构中加入了链表对象ListItem_t ,这种使用方法是用户数据中包含链表的方式。
2、指针法定位对象地址
FreeRTOS中xLIST_ITEM中加入了void * pvOwner指针用于定位包含链表的对象的地址,这种使用方法是指针法定位对象地址。
3、哨兵机制
FreeRTOS中MiniListItem_t就是哨兵(表头)。
- 代码:
/* github: liyinuo2017 author:liwei */
/**********************链表中包含户数据形式 **********************/
struct list_contain_data_def
u32 key; /*键值 */
struct list_contain_data_def * next; /*指向下一个对象*/
struct list_contain_data_def * previous; /*指向上一个对象*/
u8 user_data[100]; /*用户数据*/
list_contain_data_t;
/**********************用户数据中包含链表形式 **********************/
struct list_def
u32 key; /*键值 */
struct list_def * next; /*指向下一个对象*/
struct list_def * previous; /*指向上一个对象*/
list; /*链表 */
typedef struct list list_t; /*链表 */
struct data_contain_list_def
u8 user_data[100]; /*用户数据*/
list_t user_list /*链表*/
data_contain_list_t;
struct list_def
u32 key; /*键值 */
struct list_def * next; /*指向下一个对象*/
struct list_def * previous; /*指向上一个对象*/
void * owner; /*指向链表拥有者 */
list; /*链表 */
typedef struct list list_t; /*链表 */
struct data_contain_list_def
u8 user_data[100]; /*用户数据*/
list_t user_list /*链表*/
data_contain_list_t;
#define container_of(ptr, type, member) ( \\
const typeof( ((type *)0)->member ) *__mptr = (ptr); \\
(type *)( (char *)__mptr - offsetof(type,member) );)
struct xLIST_ITEM
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检查位*/
configLIST_VOLATILE TickType_t xItemValue; /*用于排序的值 */
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向下一项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向上一项*/
void * pvOwner; /*TCB指针 */
struct xLIST * configLIST_VOLATILE pxContainer; /*指向放置此列表项的列表 */
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检查位*/
;
typedef struct xLIST_ITEM ListItem_t; /* 列表项 */
struct xMINI_LIST_ITEM
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检查位 */
configLIST_VOLATILE TickType_t xItemValue; /*用于排序的值 */
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*指向下一项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*指向上一项*/
;
typedef struct xMINI_LIST_ITEM MiniListItem_t; /* MINI列表项 */
typedef struct xLIST
listFIRST_LIST_INTEGRITY_CHECK_VALUE /*检查位*/
volatile UBaseType_t uxNumberOfItems; /*列表项总数量*/
ListItem_t * configLIST_VOLATILE pxIndex; /*用于遍历列表*/
MiniListItem_t xListEnd; /*列表的最末项*/
listSECOND_LIST_INTEGRITY_CHECK_VALUE /*检查位*/
List_t; /* 列表 */
#define configMAX_PRIORITIES ( 10 )
PRIVILEGED_DATA static List_t pxReadyTasksLists[ configMAX_PRIORITIES ];/*就绪表 */
typedef struct tskTaskControlBlock
volatile StackType_t *pxTopOfStack; /*栈指针*/
ListItem_t xStateListItem; /*任务的状态列表项*/
ListItem_t xEventListItem; /*< 任务的事件列表项 */
UBaseType_t uxPriority; /*优先级*/
StackType_t *pxStack; /*栈起始地址 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];/*任务名 */
/*省略部分 */
tskTCB;
未完待续…
实时操作系统系列将持续更新
创作不易希望朋友们点赞,转发,评论,关注。
您的点赞,转发,评论,关注将是我持续更新的动力
作者:李巍
Github:liyinuoman2017
以上是关于嵌入式实时操作系统6——链表数据结构的主要内容,如果未能解决你的问题,请参考以下文章
《安富莱嵌入式周报》第279期:强劲的代码片段搜索工具,卡内基梅隆大学安全可靠C编码标准,Nordic发布双频WiFi6 nRF7002芯片