LiteOS:盤點那些重要的數據結構

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

雙向鏈表源代碼、示例程序代碼、開發文檔如下:

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的前序節點是*listlist->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_EACHLOS_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_SAFELOS_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_ENTRYLOS_DL_LIST_FOR_EACH_ENTRY_SAFELOS_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] 是對應任務優先級prioritypendList的雙向鏈表。會依次遍歷這個雙向鏈表&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 排序鏈表

SortLinkListLiteOS另外一個比較重要的數據結構,它在LOS_DL_LIST雙向鏈表結構體的基礎上,增加了RollNum滾動數,用於涉及時間到期、超時的業務場景。在阻塞任務是否到期,定時器是否超時場景下,非常依賴SortLinkList排序鏈表這個數據結構。LiteOS排序鏈表支持單一鏈表LOSCFG_BASE_CORE_USE_SINGLE_LIST和多鏈表LOSCFG_BASE_CORE_USE_MULTI_LIST,可以通過LiteOSmenuconfig工具更改Sortlink Option選項來配置使用單鏈表還是多鏈表,我們這裏先講述前者。

排序鏈表SortLinkList接口主要內部使用,用戶業務開發時不涉及,不對外提供接口。SortLinkList排序鏈表的代碼都在kernelbaseincludelos_sortlink_pri.h頭文件和kernelbaselos_sortlink.c實現文件中。

3.1 SortLinkList 排序鏈表結構體定義

kernelbaseincludelos_sortlink_pri.h文件中定義了兩個結構體,如下述源碼所示。

SortLinkAttribute結構體定義排序鏈表的頭結點LOS_DL_LIST *sortLink,遊標UINT16 cursorSortLinkList結構體定義排序鏈表的業務節點,除了負責雙向鏈接的成員變量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 。

點擊關注,第一時間瞭解華爲雲新鮮技術~

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