摘要:本文會給讀者介紹下LiteOS源碼中常用的幾個數據結構,包括: 雙向循環鏈表LOS_DL_LIST,優先級隊列Priority Queue,排序鏈表SortLinkList等。
在學習Huawei LiteOS
源代碼的時候,常常會遇到一些數據結構的使用。如果沒有掌握這它們的用法,閱讀LiteOS
源代碼的時候會很費解、很喫力。本文會給讀者介紹下LiteOS
源碼中常用的幾個數據結構,包括: 雙向循環鏈表LOS_DL_LIST
,優先級隊列Priority Queue
,排序鏈表SortLinkList
等。在講解時,會結合相關的繪圖,培養數據結構的平面想象能力,幫助更好的學習和理解這些數據結構用法。
本文中所涉及的LiteOS
源碼,均可以在LiteOS
開源站點https://gitee.com/LiteOS/LiteOS 獲取。
我們首先來看看使用最多的雙向循環鏈表Doubly Linked List
。
1、LOS_DL_LIST 雙向循環鏈表
雙向鏈表LOS_DL_LIST
核心的代碼都在kernelincludelos_list.h
頭文件中,包含LOS_DL_LIST
結構體定義、一些inline
內聯函數LOS_ListXXX
,還有一些雙向鏈表相關的宏定義LOS_DL_LIST_XXXX
。
雙向鏈表源代碼、示例程序代碼、開發文檔如下:
- kernelincludelos_list.h 雙向鏈表頭文件網頁獲取源碼 https://gitee.com/LiteOS/Lite...。
- demoskernelapilos_api_list.c 雙向鏈表Demo程序網頁獲取源碼 https://gitee.com/LiteOS/Lite...。
- 開發指南雙向鏈表文檔在線文檔https://gitee.com/LiteOS/Lite...
1.1 LOS_DL_LIST 雙向鏈表結構體
雙向鏈表結構體LOS_DL_LIST
定義如下。看得出來,雙向鏈表的結構非常簡單、通用、抽象,只包含前驅、後繼兩個節點,負責承上啓下的雙向鏈表作用。雙向鏈表不包任何業務數據信息,業務數據信息維護在業務的結構體中。雙向鏈表作爲業務結構體的成員使用,使用示例稍後會有講述。
typedef struct LOS_DL_LIST {
struct LOS_DL_LIST *pstPrev; /** 當前節點的指向前驅節點的指針 */
struct LOS_DL_LIST *pstNext; /** 當前節點的指向後繼節點的指針 */
} LOS_DL_LIST;
從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點,這種數據結構形式使得雙向鏈表在查找、插入、刪除等操作,對於非常方便。由於雙向鏈表的環狀結構,任何一個節點的地位都是平等的。從業務上,可以創建一個節點作爲Head
頭節點,業務結構體的鏈表節點從HEAD
節點開始掛載。從head
節點的依次遍歷下一個節點,最後一個不等於Head
節點的節點稱之爲Tail
尾節點。這個Tail
節點也是Head
節點的前驅。從Head
向前查找,可以更快的找到Tail
節點。
我們看看LiteOS
內核代碼中如何使用雙向鏈表結構體的。下面是互斥鎖結構體LosMuxCB
定義,其中包含雙向鏈表LOS_DL_LIST muxList;
成員變量:
typedef struct {
LOS_DL_LIST muxList; /** 互斥鎖的雙向鏈表*/
LosTaskCB *owner; /** 當前持有鎖的任務TCB */
UINT16 muxCount; /** 持有互斥鎖的次數 */
UINT8 muxStat; /** 互斥鎖狀態OS_MUX_UNUSED, OS_MUX_USED */
UINT32 muxId; /** 互斥鎖handler ID*/
} LosMuxCB;
雙向循環鏈表可以把各個互斥鎖鏈接起來,鏈表和其他業務成員關係如下圖所示:
LiteOS的雙向鏈表爲用戶提供下面初始化雙向列表,增加、刪除鏈表節點,判斷節點是否爲空,獲取鏈表節點,獲取鏈表所在的結構體,遍歷雙向鏈表,遍歷包含雙向鏈表的結構體等功能。我們一一來詳細的學習、分析下代碼。
1.2 LOS_DL_LIST 雙向鏈表初始化
1.2.1 LOS_ListInit(LOS_DL_LIST *list)
LOS_DL_LIST
的兩個成員*pstPrev
和*pstNext
, 是LOS_DL_LIST
結構體類型的指針。需要爲雙向鏈表節點申請長度爲sizeof(LOS_DL_LIST)
的一段內存空間。爲鏈表節點申請完畢內存後,可以調用初始化LOS_ListInit(LOS_DL_LIST *list)
方法,把這個節點鏈接爲環狀的雙向鏈表。初始化鏈表的時候,只有一個鏈表節點,這個節點的前序和後繼節點都是自身。
鏈表節點初始化爲鏈表,如圖所示:
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListInit(LOS_DL_LIST *list)
{
list->pstNext = list;
list->pstPrev = list;
}
另外,還提供了一個宏LOS_DL_LIST_HEAD
,直接定義一個雙向鏈表節點並以該節點初始化爲雙向鏈表。
#define LOS_DL_LIST_HEAD(list) LOS_DL_LIST list = { &(list), &(list) }
1.2.2 LOS_ListEmpty(LOS_DL_LIST *list)
該接口用於判斷鏈表是否爲空。如果雙向鏈表的前驅/後繼節點均爲自身,只有一個鏈表HEAD
頭節點,沒有掛載業務結構體的鏈表節點,稱該鏈表爲空鏈表。
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE BOOL LOS_ListEmpty(LOS_DL_LIST *list)
{
return (BOOL)(list->pstNext == list);
}
1.3 LOS_DL_LIST 雙向鏈表節點操作
LiteOS
雙向鏈表提供三種鏈表節點插入方法,指定鏈表節點後面插入LOS_ListAdd
、尾部插入LOS_ListTailInsert
、頭部插入LOS_ListHeadInsert
。在頭部插入的節點,從頭部開始遍歷時第一個遍歷到,從尾部插入的節點,最後一個遍歷到。
1.3.1 LOS_ListAdd(LOS_DL_LIST list, LOS_DL_LIST node)
這個API
接口往鏈表節點*list
所在的雙向鏈表中插入一個鏈表節點*node
,插入位置在鏈表節點*list
的後面。如圖所示,完成插入後,*node
的後繼節點是list->pstNext
,*node
的前序節點是*list
。list->pstNext
的前序節點是*node
,*list
的後續是*node
節點。
圖示:
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListAdd(LOS_DL_LIST *list, LOS_DL_LIST *node)
{
node->pstNext = list->pstNext;
node->pstPrev = list;
list->pstNext->pstPrev = node;
list->pstNext = node;
}
1.3.2 LOS_ListTailInsert(LOS_DL_LIST list, LOS_DL_LIST node)
這個API
接口往鏈表節點*list
所在的雙向鏈表中插入一個鏈表節點*node
,插入位置在鏈表節點*list
的前面,在list->pstPrev
節點的後面。
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListTailInsert(LOS_DL_LIST *list, LOS_DL_LIST *node)
{
LOS_ListAdd(list->pstPrev, node);
}
1.3.3 LOS_ListHeadInsert(LOS_DL_LIST list, LOS_DL_LIST node)
這個API
接口和LOS_ListAdd()
接口實現同樣的功能,往鏈表節點*list
所在的雙向鏈表中插入一個鏈表節點*node
,插入位置在鏈表節點*list
的後面。
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListHeadInsert(LOS_DL_LIST *list, LOS_DL_LIST *node)
{
LOS_ListAdd(list, node);
}
LiteOS雙向鏈表提供兩種鏈表節點的刪除方法,指定節點刪除LOS_ListDelete
、刪除並初始化爲一個新鏈表LOS_ListDelInit
。
1.3.4 LOS_ListDelete(LOS_DL_LIST *node)
這個API
接口將鏈表節點*node
從所在的雙向鏈表中刪除。節點刪除後,可能需要調用Free()
函數釋放節點所佔用的內存。如圖所示,*node
節點後繼節點的前序改爲*node
的前序,*node
節點前序節點的後續改爲*node
的後續,並把*node
節點的前序、後續節點設置爲null
。
圖示:
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListDelete(LOS_DL_LIST *node)
{
node->pstNext->pstPrev = node->pstPrev;
node->pstPrev->pstNext = node->pstNext;
node->pstNext = NULL;
node->pstPrev = NULL;
}
1.3.5 LOS_ListDelInit(LOS_DL_LIST *list)
這個API
接口將鏈表節點*list
從所在的雙向鏈表中刪除, 並把刪除後的節點重新初始化爲一個新的雙向鏈表。
*list
節點後繼節點的前序改爲*list
的前序,*list
節點前序節點的後續改爲*list
的後續。和LOS_ListDelete()
方法不同的是,併不併把*list
節點的前序、後續節點設置爲null
,而是把這個刪除的節點重新初始化爲一個新的以*list
爲頭結點的雙向鏈表。
源碼如下:
LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListDelInit(LOS_DL_LIST *list)
{
list->pstNext->pstPrev = list->pstPrev;
list->pstPrev->pstNext = list->pstNext;
LOS_ListInit(list);
}
LiteOS雙向鏈表還提供獲取鏈表節點、獲取包含鏈表的結構體地址的操作。
1.3.6 LOS_DL_LIST_LAST(object)
這個宏定義獲取鏈表的前驅節點。
源碼如下:
#define LOS_DL_LIST_LAST(object) ((object)->pstPrev)
1.3.7 LOS_DL_LIST_FIRST(object)
這個宏定義獲取鏈表的後繼節點。
源碼如下:
#define LOS_DL_LIST_FIRST(object) ((object)->pstNext)
1.3.8 LOS_OFF_SET_OF(type, member)
這個宏定義根據結構體類型名稱type
和其中的成員變量名稱member
,獲取member
成員變量相對於結構體type
的內存地址偏移量。在應用場景上,業務結構體包含雙向鏈表作爲成員,當知道雙向鏈表成員變量的內存地址時,和這個偏移量,可以進一步獲取業務結構體的內存地址。
源碼如下:
#define LOS_OFF_SET_OF(type, member) ((UINTPTR)&((type *)0)->member)
1.3.9 LOS_DL_LIST_ENTRY(item, type, member)
根據業務結構體類型名稱type
、其中的雙向鏈表成員變量名稱member
,和雙向鏈表的內存指針變量item
,使用該宏定義LOS_DL_LIST_ENTRY
可以獲取業務結構體的內存地址。
我們以實際例子演示下這個宏LOS_DL_LIST_ENTRY
是如何使用的。互斥鎖的control block
結構體LosMuxCB
在上文已經展示過其代碼,有個雙向鏈表的成員變量LOS_DL_LIST muxList
。在創建互斥鎖的方法LOS_MuxCreate()
中,⑴ 處代碼從空閒互斥鎖鏈表中獲取一個空閒的雙向鏈表節點指針地址LOS_DL_LIST *unusedMux
,把這個作爲第一個參數,結構體名稱LosMuxCB
及其成員變量muxList
,分別作爲第二、第三個參數,使用宏LOS_DL_LIST_ENTRY
可以計算出結構體的指針變量地址LosMuxCB *muxCreated
,見⑵處代碼。
LITE_OS_SEC_TEXT UINT32 LOS_MuxCreate(UINT32 *muxHandle)
{
......
LosMuxCB *muxCreated = NULL;
LOS_DL_LIST *unusedMux = NULL;
......
⑴ unusedMux = LOS_DL_LIST_FIRST(&g_unusedMuxList);
LOS_ListDelete(unusedMux);
⑵ muxCreated = LOS_DL_LIST_ENTRY(unusedMux, LosMuxCB, muxList);
......
}
從這個例子上,就比較容易理解,這個宏定義可以用於什麼樣的場景,讀者們可以閱讀查看更多使用這個宏的例子,加強理解。
源碼如下:
源碼實現上,基於雙向鏈表節點的內存地址,和雙向鏈表成員變量在結構體中的地址偏移量,可以計算出結構體的內存地址。
#define LOS_DL_LIST_ENTRY(item, type, member)
((type *)(VOID *)((CHAR *)(item) - LOS_OFF_SET_OF(type, member)))
1.4 LOS_DL_LIST 雙向循環鏈表遍歷
LiteOS
雙向循環鏈表提供兩種遍歷雙向鏈表的方法,LOS_DL_LIST_FOR_EACH
和LOS_DL_LIST_FOR_EACH_SAFE
。
1.4.1 LOS_DL_LIST_FOR_EACH(item, list)
該宏定義LOS_DL_LIST_FOR_EACH
遍歷雙向鏈表,接口的第一個入參表示的是雙向鏈表節點的指針變量,在遍歷過程中依次指向下一個鏈表節點。第二個入參是要遍歷的雙向鏈表的起始節點。這個宏是個循環條件部分,用戶的業務代碼寫在宏後面的代碼塊{}
內。
我們以實際例子來演示這個宏LOS_DL_LIST_FOR_EACH
是如何使用的。在kernelbaseschedsched_sqlos_priqueue.c
文件中,UINT32 OsPriQueueSize(UINT32 priority)
函數的片段如下:
&g_priQueueList[priority]
是我們要遍歷的雙向鏈表,curNode
指向遍歷過程中的鏈表節點,見⑴處代碼代碼。完整代碼請訪問我們的開源站點。
UINT32 OsPriQueueSize(UINT32 priority)
{
UINT32 itemCnt = 0;
LOS_DL_LIST *curNode = NULL;
......
⑴ LOS_DL_LIST_FOR_EACH(curNode, &g_priQueueList[priority]) {
......
task = OS_TCB_FROM_PENDLIST(curNode);
......
}
return itemCnt;
}
源碼如下:
#define LOS_DL_LIST_FOR_EACH(item, list)
for (item = (list)->pstNext;
(item) != (list);
item = (item)->pstNext)
1.4.2 LOS_DL_LIST_FOR_EACH_SAFE(item, next, list)
該宏定義LOS_DL_LIST_FOR_EACH_SAFE
和LOS_DL_LIST_FOR_EACH
唯一的區別就是多個入參next
, 這個參數表示遍歷到的雙向鏈表節點的下一個節點。該宏用於安全刪除,如果刪除遍歷到的item
, 不影響繼續遍歷。
源碼如下:
#define LOS_DL_LIST_FOR_EACH_SAFE(item, next, list)
for (item = (list)->pstNext, next = (item)->pstNext;
(item) != (list);
item = next, next = (item)->pstNext)
1.5 LOS_DL_LIST 遍歷包含雙向鏈表的結構體
LiteOS
雙向鏈表提供三個宏定義來遍歷包含雙向鏈表成員的結構體,LOS_DL_LIST_FOR_EACH_ENTRY
、LOS_DL_LIST_FOR_EACH_ENTRY_SAFE
和LOS_DL_LIST_FOR_EACH_ENTRY_HOOK
。
1.5.1 LOS_DL_LIST_FOR_EACH_ENTRY(item, list, type, member)
該宏定義LOS_DL_LIST_FOR_EACH_ENTRY
遍歷雙向鏈表,接口的第一個入參表示的是包含雙向鏈表成員的結構體的指針變量,第二個入參是要遍歷的雙向鏈表的起始節點,第三個入參是要獲取的結構體名稱,第四個入參是在該結構體中的雙向鏈表的成員變量名稱。
我們以實際例子來演示這個宏LOS_DL_LIST_FOR_EACH_ENTRY
是如何使用的。在kernelbaseschedsched_sqlos_priqueue.c
文件中,LosTaskCB *OsGetTopTask(VOID)
函數的片段如下。結構體LosTaskCB
包含雙向鏈表成員變量pendList
,&g_priQueueList[priority]
是對應任務優先級priority
的pendList
的雙向鏈表。會依次遍歷這個雙向鏈表&g_priQueueList[priority]
,根據遍歷到的鏈表節點,依次獲取任務結構體LosTaskCB
的指針變量newTask
,如⑴處代碼所示。
LITE_OS_SEC_TEXT_MINOR LosTaskCB *OsGetTopTask(VOID)
{
UINT32 priority;
UINT32 bitmap;
LosTaskCB *newTask = NULL;
......
⑴ LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &g_priQueueList[priority], LosTaskCB, pendList) {
......
OsPriQueueDequeue(&newTask->pendList);
......
}
......
}
源碼如下:
源碼實現上,for
循環的初始化語句item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member)
表示包含雙向鏈表成員的結構體的指針變量item
,條件測試語句&(item)->member != (list)
循環條件表示當雙向鏈表遍歷一圈到自身節點的時候,停止循環。循環更新語句item = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member))
中,使用(item)->member.pstNext
遍歷到下一個鏈表節點,然後根據這個節點獲取對應的下一個結構體的指針變量item
,直至遍歷完畢。
#define LOS_DL_LIST_FOR_EACH_ENTRY(item, list, type, member)
for (item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member);
&(item)->member != (list);
item = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member))
1.5.2LOS_DL_LIST_FOR_EACH_ENTRY_SAFE(item, next, list, type, member)
該宏定義和LOS_DL_LIST_FOR_EACH_ENTRY
唯一的區別就是多個個入參next
, 這個參數表示遍歷到的結構體的下一個結構體地址的指針變量。該宏用於安全刪除,如果刪除遍歷到的item
,不影響繼續遍歷。
源碼如下:
#define LOS_DL_LIST_FOR_EACH_ENTRY_SAFE(item, next, list, type, member)
for (item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member),
next = LOS_DL_LIST_ENTRY((item)->member->pstNext, type, member);
&(item)->member != (list);
item = next, next = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member))
1.5.3LOS_DL_LIST_FOR_EACH_ENTRY_HOOK(item, list, type, member, hook)
該宏定義和LOS_DL_LIST_FOR_EACH_ENTRY
的區別就是多了個入參hook
個鉤子函數。在每次遍歷循環中,調用該鉤子函數做些用戶定製的工作。
源碼如下:
#define LOS_DL_LIST_FOR_EACH_ENTRY_HOOK(item, list, type, member, hook)
for (item = LOS_DL_LIST_ENTRY((list)->pstNext, type, member), hook;
&(item)->member != (list);
item = LOS_DL_LIST_ENTRY((item)->member.pstNext, type, member), hook)
2、Priority Queue 優先級隊列
在任務調度模塊,就緒隊列是個重要的數據結構,就緒隊列需要支持初始化,出入隊列,從隊列獲取最高優先級任務等操作。LiteOS
調度模塊支持單一就緒隊列(Single Ready Queue)和多就緒隊列(Multiple Ready Queue),我們這裏主要講述一下單一就緒隊列。
優先級隊列Priority Queue
接口主要內部使用,用戶業務開發時不涉及,不對外提供接口。優先級隊列其實就是個雙向循環鏈表數組,提供更加方便的接口支持任務基於優先級進行調度。
優先級隊列核心的代碼都在kernelbaseincludelos_priqueue_pri.h
頭文件和kernelbaseschedsched_sqlos_priqueue.c
實現文件中。
我們來看看優先級隊列支持的操作。
2.1 Priority Queue 優先級隊列變量定義
LiteOS
支持32個優先級,取值範圍0-31,優先級數值越小優先級越大。優先級隊列在kernelbaseschedsched_sqlos_priqueue.c
文件中定義的幾個變量如下,
其中⑴表示優先級爲0的位,⑵處表示優先級隊列的雙向鏈表數組,後文會初始化爲數組的長度爲32,⑶表示優先級位圖,標誌哪些優先級就緒隊列裏有掛載的任務。
示意圖如下:
優先級位圖g_priQueueBitmap
的bit位和優先級的關係是bits=31-priority,g_priQueueList[priority]
優先級數組內容爲雙向鏈表,掛載各個優先級的處於就緒狀態的任務。
源碼如下:
#define OS_PRIORITY_QUEUE_NUM 32
⑴ #define PRIQUEUE_PRIOR0_BIT 0x80000000U
⑵ LITE_OS_SEC_BSS LOS_DL_LIST *g_priQueueList = NULL;
⑶ STATIC LITE_OS_SEC_BSS UINT32 g_priQueueBitmap;
下面我們來學習下優先級隊列支持的那些操作。
2.2 Priority Queue 優先級隊列接口
2.2.1 OsPriQueueInit(VOID)初始化
優先級隊列初始化在系統初始化的時候調用:main.c:main(void)k-->kernelinitlos_init.c:OsMain(VOID)-->kernelbaselos_task.c:OsTaskInit(VOID)-->OsPriQueueInit()
。
從下面的代碼可以看出,⑴處申請長度爲32的雙向鏈表數值申請常駐內存,運行期間不會調用Free()
接口釋放。⑴處代碼爲數組的每一個雙向鏈表元素都初始化爲雙向循環鏈表。
源碼如下:
UINT32 OsPriQueueInit(VOID)
{
UINT32 priority;
/* 系統常駐內存,運行期間不會Free釋放 */
⑴ g_priQueueList = (LOS_DL_LIST *)LOS_MemAlloc(m_aucSysMem0, (OS_PRIORITY_QUEUE_NUM * sizeof(LOS_DL_LIST)));
if (g_priQueueList == NULL) {
return LOS_NOK;
}
for (priority = 0; priority < OS_PRIORITY_QUEUE_NUM; ++priority) {
⑵ LOS_ListInit(&g_priQueueList[priority]);
}
return LOS_OK;
}
2.2.2 OsPriQueueEnqueueHead()插入就緒隊列頭部
OsPriQueueEnqueueHead()
從就緒隊列的頭部進行插入,插入得晚,但在同等優先級的任務中,會第一個調度。一起看下代碼,⑴處先判斷指定優先級priority
的就緒隊列是否爲空,如果爲空,則在⑵處更新優先級位圖。⑶處把就緒狀態的任務插入就緒隊列的頭部,以便優先調度。
源碼如下:
VOID OsPriQueueEnqueueHead(LOS_DL_LIST *priqueueItem, UINT32 priority)
{
LOS_ASSERT(priqueueItem->pstNext == NULL);
⑴ if (LOS_ListEmpty(&g_priQueueList[priority])) {
⑵ g_priQueueBitmap |= PRIQUEUE_PRIOR0_BIT >> priority;
}
⑶ LOS_ListHeadInsert(&g_priQueueList[priority], priqueueItem);
}
2.2.3 OsPriQueueEnqueue()插入就緒隊列尾部
和OsPriQueueEnqueueHead()
的區別是,把就緒狀態的任務插入就緒隊列的尾部,同等優先級的任務中,後插入的後調度。
2.2.4 OsPriQueueDequeue()就緒隊列中刪除
在任務被刪除、進入suspend
狀態,優先級調整等場景時,都需要調用接口OsPriQueueEnqueue()
把任務從優先級隊列中刪除。
我們來看下代碼,⑴把任務從優先級就緒隊列中刪除。⑵獲取刪除的任務TCB
信息,用來獲取任務的優先級。剛從優先級隊列中刪除了一個任務,⑶處代碼判斷優先級隊列是否爲空,
如果爲空,則需要執行⑷處代碼,把優先級位圖中對應的優先級bit
位置爲0。
源碼如下:
VOID OsPriQueueDequeue(LOS_DL_LIST *priqueueItem)
{
LosTaskCB *runTask = NULL;
⑴ LOS_ListDelete(priqueueItem);
⑵ runTask = LOS_DL_LIST_ENTRY(priqueueItem, LosTaskCB, pendList);
⑶ if (LOS_ListEmpty(&g_priQueueList[runTask->priority])) {
⑷ g_priQueueBitmap &= ~(PRIQUEUE_PRIOR0_BIT >> runTask->priority);
}
}
2.2.5 LOS_DL_LIST *OsPriQueueTop(VOID)獲取就緒的優先級最高的鏈表節點
這個接口可以獲取優先級就緒隊列中優先級最高的鏈表節點。⑴處判斷優先級位圖g_priQueueBitmap
是否爲0,如果爲0,說明沒有任何就緒狀態的任務,返回NULL。 ⑵處計算g_priQueueBitmap
二進制時開頭的0的數目,這個數目對應於
任務的優先級priority
,然後⑶處從&g_priQueueList[priority]
優先級隊列鏈表中獲取第一個鏈表節點。
源碼如下:
LOS_DL_LIST *OsPriQueueTop(VOID)
{
UINT32 priority;
⑴ if (g_priQueueBitmap != 0) {
⑵ priority = CLZ(g_priQueueBitmap);
⑶ return LOS_DL_LIST_FIRST(&g_priQueueList[priority]);
}
return NULL;
}
2.2.6 UINT32 OsPriQueueSize(UINT32 priority)獲取指定優先級的就緒任務的數量
這個接口可以獲取指定優先級的就緒隊列中任務的數量。⑴、⑶處代碼表示,在SMP
多核模式下,根據獲取的當前CPU編號的cpuId
,判斷任務是否屬於當前CPU核,如果不屬於,則不計數。⑵處代碼使用for
循環遍歷指定優先級就緒隊列中的鏈表節點,對遍歷到新節點則執行⑷處代碼,對計數進行進行加1操作。
源碼如下:
UINT32 OsPriQueueSize(UINT32 priority)
{
UINT32 itemCnt = 0;
LOS_DL_LIST *curNode = NULL;
#ifdef LOSCFG_KERNEL_SMP
LosTaskCB *task = NULL;
⑴ UINT32 cpuId = ArchCurrCpuid();
#endif
LOS_ASSERT(ArchIntLocked());
LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));
⑵ LOS_DL_LIST_FOR_EACH(curNode, &g_priQueueList[priority]) {
#ifdef LOSCFG_KERNEL_SMP
task = OS_TCB_FROM_PENDLIST(curNode);
⑶ if (!(task->cpuAffiMask & (1U << cpuId))) {
continue;
}
#endif
⑷ ++itemCnt;
}
return itemCnt;
}
2.2.7 LosTaskCB *OsGetTopTask(VOID)獲取就緒的優先級最高的任務
這個接口或者就緒任務隊列中優先級最高的任務。一起看下代碼,⑴、⑷處對SMP
多核做特殊處理,如果是多核,只獲取指定在當前CPU核運行的優先級最高的任務。⑵處獲取g_priQueueBitmap
優先級位圖的值,賦值給UINT32 bitmap;
。不直接操作優先級位圖的原因是什麼呢?在SMP
多核時,在高優先級任務就緒隊列裏沒有找到指定在當前CPU核運行的任務,需要執行⑹處的代碼,清零臨時優先級位圖的bit位,去低一級的優先級就緒隊列裏去查找。只能改動臨時優先級位圖,不能改變g_priQueueBitmap
。⑶處代碼對優先級最高的就緒隊列進行遍歷,如果遍歷到則執行⑸處代碼從優先級就緒隊列裏出隊,函數返回對應的LosTaskCB *newTask
。
源碼如下:
{
UINT32 priority;
UINT32 bitmap;
LosTaskCB *newTask = NULL;
#ifdef LOSCFG_KERNEL_SMP
⑴ UINT32 cpuid = ArchCurrCpuid();
#endif
⑵ bitmap = g_priQueueBitmap;
while (bitmap) {
priority = CLZ(bitmap);
⑶ LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &g_priQueueList[priority], LosTaskCB, pendList) {
#ifdef LOSCFG_KERNEL_SMP
⑷ if (newTask->cpuAffiMask & (1U << cpuid)) {
#endif
⑸ OsPriQueueDequeue(&newTask->pendList);
goto OUT;
#ifdef LOSCFG_KERNEL_SMP
}
#endif
}
⑹ bitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - priority - 1));
}
OUT:
return newTask;
}
3、SortLinkList 排序鏈表
SortLinkList
是LiteOS
另外一個比較重要的數據結構,它在LOS_DL_LIST
雙向鏈表結構體的基礎上,增加了RollNum
滾動數,用於涉及時間到期、超時的業務場景。在阻塞任務是否到期,定時器是否超時場景下,非常依賴SortLinkList
排序鏈表這個數據結構。LiteOS
排序鏈表支持單一鏈表LOSCFG_BASE_CORE_USE_SINGLE_LIST
和多鏈表LOSCFG_BASE_CORE_USE_MULTI_LIST
,可以通過LiteOS
的menuconfig
工具更改Sortlink Option
選項來配置使用單鏈表還是多鏈表,我們這裏先講述前者。
排序鏈表SortLinkList
接口主要內部使用,用戶業務開發時不涉及,不對外提供接口。SortLinkList
排序鏈表的代碼都在kernelbaseincludelos_sortlink_pri.h
頭文件和kernelbaselos_sortlink.c
實現文件中。
3.1 SortLinkList 排序鏈表結構體定義
在kernelbaseincludelos_sortlink_pri.h
文件中定義了兩個結構體,如下述源碼所示。
SortLinkAttribute
結構體定義排序鏈表的頭結點LOS_DL_LIST *sortLink
,遊標UINT16 cursor
。SortLinkList
結構體定義排序鏈表的業務節點,除了負責雙向鏈接的成員變量LOS_DL_LIST *sortLink
,還包括業務信息,UINT32 idxRollNum
,即index
索引和rollNum
滾動數。在單鏈表的排序鏈表中,idxRollNum
表示多長時間後會到期。
我們舉個例子,看下面的示意圖。排序鏈表中,有3個鏈表節點,分別在25 ticks、35 ticks、50 ticks後到期超時,已經按到期時間進行了先後排序。三個節點的idxRollNum
分別等於25 ticks、10
ticks、15 ticks。每個節點的idxRollNum
保存的不是這個節點的超時時間,而是從鏈表head
節點到該節點的所
有節點的idxRollNum
的加和,纔是該節點的超時時間。這樣設計的好處就是,隨着Tick
時間推移,只需要更新第一個節點的超時時間就好,可以好好體會一下。
示意圖如下:
源碼如下:
typedef struct {
LOS_DL_LIST sortLinkNode;
UINT32 idxRollNum;
} SortLinkList;
typedef struct {
LOS_DL_LIST *sortLink;
UINT16 cursor;
UINT16 reserved;
} SortLinkAttribute;
下面我們來學習下排序鏈表支持的那些操作。
3.2 SortLinkList 排序鏈表接口
在繼續之前我們先看下kernelbaseincludelos_sortlink_pri.h
文件中的一些單鏈表配置LOSCFG_BASE_CORE_USE_SINGLE_LIST
下的宏定義,包含滾動數最大值等,對滾動數進行加、減、減少1等操作。
源碼如下:
#define OS_TSK_SORTLINK_LOGLEN 0U
#define OS_TSK_SORTLINK_LEN 1U
#define OS_TSK_MAX_ROLLNUM 0xFFFFFFFEU
#define OS_TSK_LOW_BITS_MASK 0xFFFFFFFFU
#define SORTLINK_CURSOR_UPDATE(CURSOR)
#define SORTLINK_LISTOBJ_GET(LISTOBJ, SORTLINK) (LISTOBJ = SORTLINK->sortLink)
#define ROLLNUM_SUB(NUM1, NUM2) NUM1 = (ROLLNUM(NUM1) - ROLLNUM(NUM2))
#define ROLLNUM_ADD(NUM1, NUM2) NUM1 = (ROLLNUM(NUM1) + ROLLNUM(NUM2))
#define ROLLNUM_DEC(NUM) NUM = ((NUM) - 1)
#define ROLLNUM(NUM) (NUM)
#define SET_SORTLIST_VALUE(sortList, value) (((SortLinkList *)(sortList))->idxRollNum = (value))
3.2.1 UINT32 OsSortLinkInit() 排序鏈表初始化
在系統啓動軟件初始化,初始化任務、初始化定時器時,會分別初始化任務的排序鏈表和定時器的排序鏈表。
- kernelbaselos_task.c : UINT32 OsTaskInit(VOID)函數
`ret = OsSortLinkInit(&g_percpu[index].taskSortLink);` - kernelbaselos_swtmr.c : UINT32 OsSwtmrInit(VOID)函數
`ret = OsSortLinkInit(&g_percpu[cpuid].swtmrSortLink);`
我們看下排序鏈表初始化函數的源代碼,⑴處代碼計算需要申請多少個雙向鏈表的內存大小,對於單鏈表的排序鏈表,OS_TSK_SORTLINK_LOGLEN
爲0,爲一個雙向鏈表申請內存大小即可。然後申請內存,初始化申請的內存區域爲0等,⑵處把申請的雙向鏈表節點賦值給sortLinkHeader
的鏈表節點,作爲排序鏈表的頭節點,然後調用LOS_ListInit()
函數初始化爲雙向循環鏈表。
源碼如下:
LITE_OS_SEC_TEXT_INIT UINT32 OsSortLinkInit(SortLinkAttribute *sortLinkHeader)
{
UINT32 size;
LOS_DL_LIST *listObject = NULL;
⑴ size = sizeof(LOS_DL_LIST) << OS_TSK_SORTLINK_LOGLEN;
listObject = (LOS_DL_LIST *)LOS_MemAlloc(m_aucSysMem0, size); /* system resident resource */
if (listObject == NULL) {
return LOS_NOK;
}
(VOID)memset_s(listObject, size, 0, size);
⑵ sortLinkHeader->sortLink = listObject;
LOS_ListInit(listObject);
return LOS_OK;
}
3.2.2 VOID OsAdd2SortLink() 排序鏈表插入
在任務等待互斥鎖、信號量等資源阻塞時,定時器啓動時,這些需要等待指定時間的任務、定時器等,都會加入對應的排序鏈表。
我們一起看下代碼,包含2個參數,第一個參數sortLinkHeader
用於指定排序鏈表的頭結點,第二個參數sortList
是待插入的鏈表節點,此時該節點的滾動數等於對應阻塞任務或定時器的超時時間。
⑴處代碼處理滾動數超大的場景,如果滾動數大於OS_TSK_MAX_ROLLNUM
,則設置滾動數等於OS_TSK_MAX_ROLLNUM
。⑵處代碼,如果排序鏈表爲空, 則把鏈表節點尾部插入。如果排序鏈表不爲空,則執行⑶處代碼,獲取排序鏈表上的下一個節點SortLinkList *listSorted
。⑷、⑸ 處代碼,如果待插入節點的滾動數大於排序鏈表的下一個節點的滾動數,則把待插入節點的滾動數減去下一個節點的滾動數,並繼續執行⑹處代碼,繼續與下下一個節點進行比較。否則,如果待插入節點的滾動數小於排序鏈表的下一個節點的滾動數,則把下一個節點的滾動數減去待插入節點的滾動數,然後跳出循環,繼續執行⑺處代碼,完成待插入節點的插入。插入過程,可以結合上文的示意圖進行理解。
源碼如下:
LITE_OS_SEC_TEXT VOID OsAdd2SortLink(const SortLinkAttribute *sortLinkHeader, SortLinkList *sortList)
{
SortLinkList *listSorted = NULL;
LOS_DL_LIST *listObject = NULL;
⑴ if (sortList->idxRollNum > OS_TSK_MAX_ROLLNUM) {
SET_SORTLIST_VALUE(sortList, OS_TSK_MAX_ROLLNUM);
}
listObject = sortLinkHeader->sortLink;
⑵ if (listObject->pstNext == listObject) {
LOS_ListTailInsert(listObject, &sortList->sortLinkNode);
} else {
⑶ listSorted = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
do {
⑷ if (ROLLNUM(listSorted->idxRollNum) <= ROLLNUM(sortList->idxRollNum)) {
ROLLNUM_SUB(sortList->idxRollNum, listSorted->idxRollNum);
} else {
⑸ ROLLNUM_SUB(listSorted->idxRollNum, sortList->idxRollNum);
break;
}
⑹ listSorted = LOS_DL_LIST_ENTRY(listSorted->sortLinkNode.pstNext, SortLinkList, sortLinkNode);
} while (&listSorted->sortLinkNode != listObject);
⑺ LOS_ListTailInsert(&listSorted->sortLinkNode, &sortList->sortLinkNode);
}
}
3.2.3 VOID OsDeleteSortLink() 排序鏈表刪除
當任務恢復、刪除,定時器停止的時候,會從對應的排序鏈表中刪除。
我們一起閱讀下刪除函數的源代碼,包含2個參數,第一個參數sortLinkHeader
用於指定排序鏈表的頭結點,第二個參數sortList
是待刪除的鏈表節點。
⑴處是獲取排序鏈表的頭結點listObject
,⑵處代碼檢查要刪除的節點是否在排序鏈表裏,否則輸出錯誤信息和回溯棧信息。⑶處代碼判斷是否排序鏈表裏只有一個業務節點,如果只有一個節點,直接執行⑸處代碼刪除該節點即可。如果排序鏈表裏有多個業務節點,則執行⑷處代碼獲取待刪除節點的下一個節點nextSortList
,把刪除節點的滾動數加到下一個節點的滾動數裏,然後執行⑸處代碼執行刪除操作。
源碼如下:
LITE_OS_SEC_TEXT VOID OsDeleteSortLink(const SortLinkAttribute *sortLinkHeader, SortLinkList *sortList)
{
LOS_DL_LIST *listObject = NULL;
SortLinkList *nextSortList = NULL;
⑴ listObject = sortLinkHeader->sortLink;
⑵ OsCheckSortLink(listObject, &sortList->sortLinkNode);
⑶ if (listObject != sortList->sortLinkNode.pstNext) {
⑷ nextSortList = LOS_DL_LIST_ENTRY(sortList->sortLinkNode.pstNext, SortLinkList, sortLinkNode);
ROLLNUM_ADD(nextSortList->idxRollNum, sortList->idxRollNum);
}
⑸ LOS_ListDelete(&sortList->sortLinkNode);
}
3.2.4 UINT32 OsSortLinkGetNextExpireTime() 獲取下一個超時到期時間
在Tickless
特性,會使用此方法獲取下一個超時到期時間。
我們一起閱讀下源代碼,包含1個參數,sortLinkHeader
用於指定排序鏈表的頭結點。
⑴處是獲取排序鏈表的頭結點listObject
,⑵處代碼判斷排序鏈表是否爲空,如果排序鏈表爲空,則返回OS_INVALID_VALUE
。如果鏈表不爲空,⑶處代碼獲取排序鏈表的第一個業務節點,然後獲取其滾動數,即過期時間,進行返回。
源碼如下:
LITE_OS_SEC_TEXT UINT32 OsSortLinkGetNextExpireTime(const SortLinkAttribute *sortLinkHeader)
{
UINT32 expireTime = OS_INVALID_VALUE;
LOS_DL_LIST *listObject = NULL;
SortLinkList *listSorted = NULL;
⑴ listObject = sortLinkHeader->sortLink;
⑵ if (!LOS_ListEmpty(listObject)) {
⑶ listSorted = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
expireTime = listSorted->idxRollNum;
}
return expireTime;
}
3.2.5 OsSortLinkGetTargetExpireTime() 獲取指定節點的超時時間
定時器獲取剩餘超時時間函數LOS_SwtmrTimeGet()
會調用函數OsSortLinkGetTargetExpireTime()
獲取指定節點的超時時間。
我們一起看下代碼,包含2個參數,第一個參數sortLinkHeader
用於指定排序鏈表的頭結點,第二個參數targetSortList
是待獲取超時時間的目標鏈表節點。
⑴處代碼獲取目標節點的滾動數。⑵處代碼獲取排序鏈表的頭結點listObject
,⑶處代碼獲取排序鏈表上的下一個節點SortLinkList *listSorted
。⑷處循環代碼,當下一個節點不爲目標鏈表節點的時候,依次循環,並執行⑸處代碼把循環遍歷的各個節點的滾動數相加,最終的計算結果即爲目標節點的超時時間。
源碼如下:
LITE_OS_SEC_TEXT_MINOR UINT32 OsSortLinkGetTargetExpireTime(const SortLinkAttribute *sortLinkHeader,
const SortLinkList *targetSortList)
{
SortLinkList *listSorted = NULL;
LOS_DL_LIST *listObject = NULL;
⑴ UINT32 rollNum = targetSortList->idxRollNum;
⑵ listObject = sortLinkHeader->sortLink;
⑶ listSorted = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
⑷ while (listSorted != targetSortList) {
⑸ rollNum += listSorted->idxRollNum;
listSorted = LOS_DL_LIST_ENTRY((listSorted->sortLinkNode).pstNext, SortLinkList, sortLinkNode);
}
return rollNum;
}
3.2.6 VOID OsSortLinkUpdateExpireTime() 更新超時時間
在Tickless
特性,會使用此方法更新超時時間。Tickless
休眠sleep
時,需要把休眠的ticks
數目從排序鏈表裏減去。調用此方法的函數會保障減去的ticks
數小於節點的滾動數。
我們一起閱讀下源代碼,包含2個參數,第一個參數sleepTicks
是休眠的ticks
數,第二個參數sortLinkHeader
用於指定排序鏈表的頭結點。
⑴處獲取排序鏈表的頭結點listObject
,⑵處代碼獲取下一個鏈表節點sortList
,這個也是排序鏈表的第一個業務節點,然後把該節點的滾動數減去sleepTicks - 1
完成超時時間更新。
源碼如下:
LITE_OS_SEC_TEXT VOID OsSortLinkUpdateExpireTime(UINT32 sleepTicks, SortLinkAttribute *sortLinkHeader)
{
SortLinkList *sortList = NULL;
LOS_DL_LIST *listObject = NULL;
if (sleepTicks == 0) {
return;
}
⑴ listObject = sortLinkHeader->sortLink;
⑵ sortList = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
ROLLNUM_SUB(sortList->idxRollNum, sleepTicks - 1);
}
3.3 SortLinkList 排序鏈表和Tick時間關係
任務、定時器加入排序鏈表後,隨時時間推移,一個tick
一個tick
的逝去,排序鏈表中的滾動數是如何更新的呢?
我們看看Tick
中斷的處理函數VOID OsTickHandler(VOID)
,該函數在kernelbaselos_tick.c
文件裏。
當時間每走過一個tick
,會調用該中斷處理函數,代碼片段中的⑴、⑵處的代碼分別掃描任務和定時器,檢查和更新時間。
LITE_OS_SEC_TEXT VOID OsTickHandler(VOID)
{
UINT32 intSave;
TICK_LOCK(intSave);
g_tickCount[ArchCurrCpuid()]++;
TICK_UNLOCK(intSave);
......
⑴ OsTaskScan(); /* task timeout scan */
#if (LOSCFG_BASE_CORE_SWTMR == YES)
⑵ OsSwtmrScan();
#endif
}
我們以OsTaskScan()
爲例,快速瞭解下排序鏈表和tick
時間的關係。函數在kernelbaselos_task.c
文件中,函數代碼片段如下:
⑴處代碼獲取任務排序鏈表的第一個節點,然後執行下一行代碼把該節點的滾動數減去1。⑵處代碼循環遍歷排序鏈表,如果滾動數爲0,即時間到期了,會調用LOS_ListDelete()
函數從從排序鏈表中刪除,然後執行⑶處代碼,獲取對應的taskCB
,然後進一步進行業務處理。讀者可以自行查看更多代碼,後續的文章中也會對任務、定時器進行專題進行講解。
LITE_OS_SEC_TEXT VOID OsTaskScan(VOID)
{
SortLinkList *sortList = NULL;
......
LOS_DL_LIST *listObject = NULL;
SortLinkAttribute *taskSortLink = NULL;
taskSortLink = &OsPercpuGet()->taskSortLink;
SORTLINK_CURSOR_UPDATE(taskSortLink->cursor);
SORTLINK_LISTOBJ_GET(listObject, taskSortLink);
......
⑴ sortList = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
ROLLNUM_DEC(sortList->idxRollNum);
⑵ while (ROLLNUM(sortList->idxRollNum) == 0) {
LOS_ListDelete(&sortList->sortLinkNode);
⑶ taskCB = LOS_DL_LIST_ENTRY(sortList, LosTaskCB, sortList);
......
sortList = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
}
......
}
小結
掌握LiteOS
內核的雙向循環鏈表LOS_DL_LIST
,優先級隊列Priority Queue
,排序鏈表SortLinkList
等重要的數據結構,給進一步學習、分析LiteOS
源代碼打下了基礎,讓後續的學習更加容易。後續也會陸續推出更多的分享文章,敬請期待,也歡迎大家分享學習使用LiteOS的心得,有任何問題、建議,都可以留言給我們: https://gitee.com/LiteOS/Lite... 。爲了更容易找到LiteOS
代碼倉,建議訪問 https://gitee.com/LiteOS/LiteOS ,關注Watch
、點贊Star
、並Fork
到自己賬戶下,如下圖,謝謝。
本文分享自華爲雲社區《LiteOS內核源碼分析系列一 盤點那些重要的數據結構 》,原文作者:zhushy 。