目錄
在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(;;)
{
/* 啥事不幹 */
}
}
屏蔽代碼解除屏蔽