摘要:本文會給讀者介紹下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 。