FreeRTOS 筆記之③:數據結構-- 列表和表項(鏈表和節點)

目錄

1. C語言鏈表

1.1 單向鏈表

1.2 雙向鏈表

1.3 鏈表的操作

1.4 鏈表與數組的對比

2. FreeRTOS中鏈表的實現

2.1 實現鏈表節點

2.2 實現鏈表根節點

2.2.1 根節點數據結構

2.2.2 鏈表根節點的初始化

2.2.3 將節點插入到鏈表的尾部

2.2.4 將節點按照升序排列插入到鏈表

2.2.5 將節點從鏈表刪除

2.2.6 節點帶參宏小函數

3. 鏈表節點插入實驗


在FreeRTOS中存在着大量的基礎數據結構列表和列表項的操作,要想讀懂FreeRTOS的源碼或者從0到1開始實現FreeRTOS,就必須弄懂列表和列表項的操作,其實也沒那麼難。

列表和列表項是直接從FreeRTOS源碼的註釋中的list 和 list item翻譯過來的,其實就是對應我們C語言當中的鏈表和節點,在後續的講解中,我們說的鏈表就是列表,節點就是列表項。

1. C語言鏈表

鏈表作爲C語言中一種基礎的數據結構,在平時寫程序的時候用的並不多,但在操作系統裏面使用的非常多。鏈表就好比一個圓形的晾衣架,晾衣架上面有很多鉤子,鉤子首尾相連。鏈表也是,鏈表由節點組成,節點與節點之間首尾相連。

晾衣架的鉤子本身不能代表很多東西,但是鉤子本身卻可以掛很多東西。同樣,鏈表也類似,鏈表的節點本身不能存儲太多東西,或者說鏈表的節點本來就不是用來存儲大量數據的,但是節點跟晾衣架的鉤子一樣,可以掛很多數據。

鏈表分爲單向鏈表和雙向鏈表,單向鏈表很少用,使用最多的還是雙向鏈表。

1.1 單向鏈表

單向鏈表示意圖。該鏈表中共有n個節點,前一個節點都有一個箭頭指向後一個節點,首尾相連,組成一個圈。

 節點本身必須包含一個節點指針,用於指向後一個節點,除了這個節點指針是必須有的之外,節點本身還可以攜帶一些私有信息,怎麼攜帶?

節點都是一個自定義類型的數據結構,在這個數據結構裏面可以有單個的數據、數組、指針數據和自定義的結構體數據類型等等信息。

struct node {
    struct node *pNext;     /* 指向鏈表的下一個節點 */
    char cData;             /* 單個數據 */
    unsigned char array[];  /* 數組數據 */
    unsigned long * ptr;    /* 指針數據 */
    struct usrStruct xData; /* 自定義結構體數據 */
    /* ... ...  */
};

除了struct node *next 這個節點指針之外,剩下的成員都可以理解爲節點攜帶的數據,但是這種方法很少用。通常的做法是節點裏面只包含一個用於指向下一個節點的指針。要通過鏈表存儲的數據內嵌一個節點即可,這些要存儲的數據通過這個內嵌的節點即可掛接到鏈表中,就好像晾衣架的鉤子一樣,把衣服掛接到晾衣架中。

struct node {
    struct node *pNext;     /* 指向鏈表的下一個節點 */
};

1.2 雙向鏈表

雙向鏈表與單向鏈表的區別就是節點中有兩個節點指針,分別指向前後兩個節點,其它完全一樣。有關雙向鏈表的文字描述參考單向鏈表小節即可。

1.3 鏈表的操作

鏈表常規的操作就是節點的插入和刪除,爲了順利的插入,通常一條鏈表我們會人爲地規定一個根節點,這個根節點稱爲生產者。通常根節點還會有一個節點計數器,用於統計整條鏈表的節點個數。

有關鏈表節點的刪除和操作的代碼講解這裏先略過,具體的可參考本章接下來的“FreeRTO中鏈表的實現”小節,在這個小節裏面會有非常詳細的講解,這裏我們先建立概念爲主。

1.4 鏈表與數組的對比

兩者的示意圖

鏈表是通過節點把離散的數據鏈接成一個表,通過對節點的插入和刪除操作從而實現對數據的存取。而數組是通過開闢一段連續的內存來存儲數據,這是數組和鏈表最大的區別。

數組的每個成員對應鏈表的節點,成員和節點的數據類型可以是標準的C類型或者是用戶自定義的結構體。

數組有起始地址和結束地址,而鏈表是一個圈,沒有頭和尾之分,但是爲了方便節點的插入和刪除操作會人爲的規定一個根節點。 

2. FreeRTOS中鏈表的實現

FreeRTOS中與鏈表相關的操作均在list.h和list.c這兩個文件中實現,list.h第一次使用需要在include文件夾下面新建然後添加到工程freertos/source這個組文件,list.c第一次使用需要在freertos文件夾下面新建然後添加到工程freertos/source這個組文件。

2.1 實現鏈表節點

  • 鏈表節點的數據結構在list.h中定義
/* 節點結構體定義 */
struct xLIST_ITEM
{
	TickType_t xItemValue;              /* 輔助值,用於幫助節點做順序排列 */			
	struct xLIST_ITEM *  pxNext;        /* 指向鏈表下一個節點 */		
	struct xLIST_ITEM *  pxPrevious;    /* 指向鏈表前一個節點 */	
	void * pvOwner;			    /* 指向擁有該節點的內核對象,通常是TCB */
	void *  pvContainer;		    /* 指向該節點所在的鏈表 */
};
typedef struct xLIST_ITEM ListItem_t;       /* 節點數據類型重定義 */

  •  鏈表節點初始化
/* 節點初始化 */
void vListInitialiseItem( ListItem_t * const pxItem )
{
	/* 初始化該節點所在的鏈表爲空,表示節點還沒有插入任何鏈表 */
	pxItem->pvContainer = NULL;
}

鏈表節點ListItem_t總共有5個成員,但是初始化的時候只需將pvContainer初始化爲空即可,表示該節點還沒有插入到任何鏈表。

2.2 實現鏈表根節點

2.2.1 根節點數據結構

鏈表根節點的數據結構在list.h中定義


/* 鏈表結構體定義 */
typedef struct xLIST
{
	UBaseType_t uxNumberOfItems;            /* 鏈表節點計數器 */
	ListItem_t *  pxIndex;			/* 鏈表節點索引指針 */
	MiniListItem_t xListEnd;		/* 鏈表最後一個節點 */
} List_t;

  • 鏈表節點計數器,用於表示該鏈表下有多少個節點,根節點除外。 
  • 鏈表節點索引指針,用於遍歷節點
  • 鏈表最後一個節點。我們知道,鏈表是首尾相連的,是一個圈,首就是尾,尾就是首,這裏從字面上理解就是鏈表的最後一個節點,實際也就是鏈表的第一個節點,我們稱之爲生產者。該生產者的數據類型是一個精簡的節點,也在list.h中定義
/* mini節點結構體定義,作爲雙向鏈表的結尾
   因爲雙向鏈表是首尾相連的,頭即是尾,尾即是頭 */
struct xMINI_LIST_ITEM
{
	TickType_t xItemValue;                      /* 輔助值,用於幫助節點做升序排列 */
	struct xLIST_ITEM *  pxNext;                /* 指向鏈表下一個節點 */
	struct xLIST_ITEM *  pxPrevious;            /* 指向鏈表前一個節點 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;      /* 最小節點數據類型重定義 */

2.2.2 鏈表根節點的初始化

初始化函數在list.c中實現

/* 鏈表根節點初始化 */
void vListInitialise( List_t * const pxList )
{
	/* 將鏈表索引指針指向最後一個節點 */
	pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );

	/* 將鏈表最後一個節點的輔助排序的值設置爲最大,確保該節點就是鏈表的最後節點 */
	pxList->xListEnd.xItemValue = portMAX_DELAY;

    /* 將最後一個節點的pxNext和pxPrevious指針均指向節點自身,表示鏈表爲空 */
	pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
	pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );

	/* 初始化鏈表節點計數器的值爲0,表示鏈表爲空 */
	pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}

  • 將鏈表索引指針指向最後一個節點,即第一個節點,或者第零個節點更準確,因爲這個節點不會算入節點計數器的值。 
  • 將鏈表最後(也可以理解爲第一)一個節點的輔助排序的值設置爲最大,確保該節點就是鏈表的最後節點(也可以理解爲第一)
  • 將最後一個節點(也可以理解爲第一)的pxNext和pxPrevious指針均指向節點自身,表示鏈表爲空。
  • 初始化鏈表節點計數器的值爲0,表示鏈表爲空。

2.2.3 將節點插入到鏈表的尾部

將節點插入到鏈表的尾部(可以理解爲頭部)就是將一個新的節點插入到一個空的鏈表

/* 將節點插入到鏈表的尾部 */
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t * const pxIndex = pxList->pxIndex;

	pxNewListItem->pxNext = pxIndex;
	pxNewListItem->pxPrevious = pxIndex->pxPrevious;
	pxIndex->pxPrevious->pxNext = pxNewListItem;
	pxIndex->pxPrevious = pxNewListItem;

	/* 記住該節點所在的鏈表 */
	pxNewListItem->pvContainer = ( void * ) pxList;

	/* 鏈表節點計數器++ */
	( pxList->uxNumberOfItems )++;
}

2.2.4 將節點按照升序排列插入到鏈表

將節點按照升序排列插入到鏈表,如果有兩個節點的值相同,則新節點在舊節點的後面插入

/* 將節點按照升序排列插入到鏈表 */
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
	ListItem_t *pxIterator;
	
	/* 獲取節點的排序輔助值 */
	const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;

	/* 尋找節點要插入的位置 */
	if( xValueOfInsertion == portMAX_DELAY )
	{
		pxIterator = pxList->xListEnd.pxPrevious;
	}
	else
	{
		for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd );
		     pxIterator->pxNext->xItemValue <= xValueOfInsertion; 
			 pxIterator = pxIterator->pxNext )
		{
			/* 沒有事情可做,不斷迭代只爲了找到節點要插入的位置 */			
		}
	}

	pxNewListItem->pxNext = pxIterator->pxNext;
	pxNewListItem->pxNext->pxPrevious = pxNewListItem;
	pxNewListItem->pxPrevious = pxIterator;
	pxIterator->pxNext = pxNewListItem;

	/* 記住該節點所在的鏈表 */
	pxNewListItem->pvContainer = ( void * ) pxList;

	/* 鏈表節點計數器++ */
	( pxList->uxNumberOfItems )++;
}

2.2.5 將節點從鏈表刪除

將節點從鏈表刪除具體實現見代碼。假設將一個有三個節點的鏈表中的中間節點節點刪除


/* 將節點從鏈表中刪除 */
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
	/* 獲取節點所在的鏈表 */
	List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer;

	pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
	pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;

	/* Make sure the index is left pointing to a valid item. */
	if( pxList->pxIndex == pxItemToRemove )
	{
		pxList->pxIndex = pxItemToRemove->pxPrevious;
	}

	/* 初始化該節點所在的鏈表爲空,表示節點還沒有插入任何鏈表 */
	pxItemToRemove->pvContainer = NULL;
	
	/* 鏈表節點計數器-- */
	( pxList->uxNumberOfItems )--;

	/* 返回鏈表中剩餘節點的個數 */
	return pxList->uxNumberOfItems;
}

2.2.6 節點帶參宏小函數

/* 初始化節點的擁有者 */
#define listSET_LIST_ITEM_OWNER( pxListItem, pxOwner )		( ( pxListItem )->pvOwner = ( void * ) ( pxOwner ) )
/* 獲取節點擁有者 */
#define listGET_LIST_ITEM_OWNER( pxListItem )	( ( pxListItem )->pvOwner )

/* 初始化節點排序輔助值 */
#define listSET_LIST_ITEM_VALUE( pxListItem, xValue )	( ( pxListItem )->xItemValue = ( xValue ) )

/* 獲取節點排序輔助值 */
#define listGET_LIST_ITEM_VALUE( pxListItem )	( ( pxListItem )->xItemValue )

/* 獲取鏈表根節點的節點計數器的值 */
#define listGET_ITEM_VALUE_OF_HEAD_ENTRY( pxList )	( ( ( pxList )->xListEnd ).pxNext->xItemValue )

/* 獲取鏈表的入口節點 */
#define listGET_HEAD_ENTRY( pxList )	( ( ( pxList )->xListEnd ).pxNext )

/* 獲取鏈表的第一個節點 */
#define listGET_NEXT( pxListItem )	( ( pxListItem )->pxNext )

/* 獲取鏈表的最後一個節點 */
#define listGET_END_MARKER( pxList )	( ( ListItem_t const * ) ( &( ( pxList )->xListEnd ) ) )

/* 判斷鏈表是否爲空 */
#define listLIST_IS_EMPTY( pxList )	( ( BaseType_t ) ( ( pxList )->uxNumberOfItems == ( UBaseType_t ) 0 ) )

/* 獲取鏈表的節點數 */
#define listCURRENT_LIST_LENGTH( pxList )	( ( pxList )->uxNumberOfItems )

/* 獲取鏈表節點的OWNER,即TCB */
#define listGET_OWNER_OF_NEXT_ENTRY( pxTCB, pxList )										\
{																							\
	List_t * const pxConstList = ( pxList );											    \
	/* 節點索引指向鏈表第一個節點調整節點索引指針,指向下一個節點,
    如果當前鏈表有N個節點,當第N次調用該函數時,pxInedex則指向第N個節點 */\
	( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;							\
	/* 當前鏈表爲空 */                                                                       \
	if( ( void * ) ( pxConstList )->pxIndex == ( void * ) &( ( pxConstList )->xListEnd ) )	\
	{																						\
		( pxConstList )->pxIndex = ( pxConstList )->pxIndex->pxNext;						\
	}																						\
	/* 獲取節點的OWNER,即TCB */                                                             \
	( pxTCB ) = ( pxConstList )->pxIndex->pvOwner;											 \
}

#define listGET_OWNER_OF_HEAD_ENTRY( pxList )  ( (&( ( pxList )->xListEnd ))->pxNext->pvOwner )

雙向鏈表的插入和刪除,理解起來會感覺比較繞,個人認爲需要找好基準,這個基準就是根節點,根節點既可以是首,亦可以是尾,通常情況下,遍歷整個鏈表都是從“頭”開始,而這個“頭”,就是根節點,因此,從這方面考慮的話,理解成頭似乎更容易接受!

3. 鏈表節點插入實驗

直接看示例代碼,比較簡單。

#include "list.h"

/*
*************************************************************************
*                              全局變量
*************************************************************************
*/

/* 定義鏈表根節點 */
struct xLIST       List_Test;

/* 定義節點 */
struct xLIST_ITEM  List_Item1;
struct xLIST_ITEM  List_Item2;
struct xLIST_ITEM  List_Item3;
struct xLIST_ITEM  List_Item4;
struct xLIST_ITEM  List_Item5;


/*
************************************************************************
*                                main函數
************************************************************************
*/
/*
* 注意事項:1、該工程使用軟件仿真,debug需選擇 Ude Simulator
*           2、在Target選項卡里面把晶振Xtal(Mhz)的值改爲25,默認是12,
*              改成25是爲了跟system_ARMCM3.c中定義的__SYSTEM_CLOCK相同,確保仿真的時候時鐘一致
*/
int main(void)
{	
	
    /* 鏈表根節點初始化 */
    vListInitialise( &List_Test );
    
    /* 節點1初始化 */
    vListInitialiseItem( &List_Item1 );
    List_Item1.xItemValue = 1;
//    List_Item1.xItemValue = 3;

    /* 節點2初始化 */    
    vListInitialiseItem( &List_Item2 );
    List_Item2.xItemValue = 2;
//    List_Item2.xItemValue = 2;
        
    /* 節點3初始化 */
    vListInitialiseItem( &List_Item3 );
    List_Item3.xItemValue = 3;
//    List_Item3.xItemValue = 1;

    /* 節點4初始化 */
    vListInitialiseItem( &List_Item4 );
    List_Item4.xItemValue = 4;

    /* 節點5初始化 */
    vListInitialiseItem( &List_Item5 );
    List_Item5.xItemValue = 5;

    /* 將節點插入鏈表,按照升序排列 */
    vListInsert( &List_Test, &List_Item2 );    
    vListInsert( &List_Test, &List_Item1 );
    vListInsert( &List_Test, &List_Item3 );    
    vListInsertEnd( &List_Test, &List_Item4 );
    vListInsertEnd( &List_Test, &List_Item5 );

    for(;;)
	{
		/* 啥事不幹 */
	}
}

屏蔽代碼解除屏蔽

https://download.csdn.net/download/xiewinter/11874146

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章