LiteOS核心原始碼分析系列一 盤點那些重要的資料結構 -- SortLinkList
在學習Huawei LiteOS原始碼的時候,常常會遇到一些資料結構的使用。如果沒有掌握這它們的用法,閱讀LiteOS原始碼的時候會很費解、很吃力。本文會給讀者介紹下LiteOS原始碼中常用的幾個資料結構,包括: 雙向迴圈連結串列LOS_DL_LIST,優先順序佇列Priority Queue,排序連結串列SortLinkList等。在講解時,會結合相關的繪圖,培養資料結構的平面想象能力,幫助更好的學習和理解這些資料結構用法。
本文中所涉及的LiteOS原始碼,均可以在LiteOS開源站點https://gitee.com/LiteOS/LiteOS 獲取。
這是第三部分,也是最後一部分,我們來看看SortLinkList 排序連結串列。
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排序連結串列的程式碼都在kernel\base\include\los_sortlink_pri.h標頭檔案和kernel\base\los_sortlink.c實現檔案中。
3.1 SortLinkList 排序連結串列結構體定義在kernel\base\include\los_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、10ticks、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 排序連結串列介面在繼續之前我們先看下kernel\base\include\los_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() 排序連結串列初始化在系統啟動軟體初始化,初始化任務、初始化定時器時,會分別初始化任務的排序連結串列和定時器的排序連結串列。
kernel\base\los_task.c : UINT32 OsTaskInit(VOID)函式 `ret = OsSortLinkInit(&g_percpu[index].taskSortLink);`kernel\base\los_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() 排序連結串列刪除當任務恢復、刪除,定時器停止的時候,會從對應的排序連結串列中刪除。
⑴處是獲取排序連結串列的頭結點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),該函式在kernel\base\los_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時間的關係。函式在kernel\base\los_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/LiteOS/issues 。為了更容易找到LiteOS程式碼倉,建議訪問 https://gitee.com/LiteOS/LiteOS ,關注Watch、點贊Star、並Fork到自己賬戶下,如下圖,謝謝。