鴻蒙輕內核M核源碼分析系列一 數據結構-雙向循環鏈表

目錄:node

一、雙向循環列表git

二、初始化雙向列表安全

三、判斷空列表數據結構

四、插入雙向鏈表節點函數

五、刪除雙向鏈表節點post

六、獲取雙向鏈表節點學習

七、遍歷雙向循環鏈表節點測試

八、獲取鏈表節點所在結構體url

九、遍歷包含雙向鏈表的結構體spa

在學習OpenHarmony鴻蒙輕內核源代碼的時候,經常會遇到一些數據結構的使用。若是沒有掌握它們的用法,會致使閱讀源代碼時很費解、很吃力。本文會給讀者介紹源碼中重要的數據結構,雙向循環鏈表Doubly Linked List。在講解時,會結合數據結構相關繪圖,培養讀者們的數據結構的平面想象能力,幫助更好的學習和理解這些數據結構的用法。

本文中所涉及的源碼,以OpenHarmony LiteOS-M內核爲例,都可以在開源站點gitee.com/openharmony… 獲取。

1 雙向循環鏈表

雙向鏈表LOS_DL_LIST的源代碼在utils\los_list.h雙向鏈表頭文件中,包含LOS_DL_LIST結構體定義、inline內聯函數LOS_ListXXX,還有相關的函數宏定義LOS_DL_LIST_XXXX。雙向鏈表頭文件能夠網頁訪問utils/los_list.h,也能夠檢出到本地閱讀。

1.1 雙向鏈表結構體

雙向鏈表節點結構體LOS_DL_LIST定義以下。其結構很是簡單、通用、抽象,只包含前驅、後繼兩個節點,負責承上啓下的雙向鏈表做用。雙向鏈表不包含任何業務數據信息,通常不會單獨使用。一般,雙向鏈表節點和業務數據信息做爲結構體成員,一塊兒組成業務結構體來使用,使用示例稍後會有講述。

typedef struct LOS_DL_LIST {
    struct LOS_DL_LIST *pstPrev; /** 指向當前鏈表節點的前驅節點的指針 */
    struct LOS_DL_LIST *pstNext; /** 指向當前鏈表節點的後繼節點的指針 */
} LOS_DL_LIST;

從雙向鏈表中的任意一個節點開始,均可以很方便地訪問它的前驅節點和後繼節點,這種環狀數據結構形式使得雙向鏈表在查找、插入、刪除等操做上很是方便。業務場景使用雙向鏈表時,能夠定義一個LOS_DL_LIST類型的全局變量做爲雙向循環鏈表Head頭結點,業務結構體的鏈表成員節點依次掛載在頭結點上。還有些業務結構體的雙向鏈表節點做爲Head頭節點,依次掛載其餘業務結構體的鏈表成員節點。從Head節點能夠依次遍歷下一個節點,Head節點的前驅節點就是Tail尾節點。

下面經過鴻蒙輕內核代碼中互斥鎖結構體LosMuxCB定義,來了解如何使用雙向鏈表結構體:

typedef struct {
    UINT8 muxStat;       /**< 互斥鎖狀態  */
    UINT16 muxCount;     /**< 互斥鎖當前被持有的次數 */
    UINT32 muxID;        /**< 互斥鎖編號ID */
    LOS_DL_LIST muxList; /**< 互斥鎖的雙向鏈表 */
    LosTaskCB *owner;    /**< 當前持有鎖的任務TCB */
    UINT16 priority;     /**< 持有互斥鎖的任務優先級 */
} LosMuxCB;

互斥鎖結構體中包括雙向鏈表LOS_DL_LIST muxList成員變量和其餘包含互斥鎖業務信息的成員變量,這裏經過雙向鏈表把各個互斥鎖連接起來,掛載在頭結點LOS_DL_LIST g_unusedMuxList;經過其餘業務成員變量承載業務數據,鏈表和其餘業務成員關係以下圖所示:

LOS_DL_DEMO

2 初始化雙向鏈表

2.1 LOS_ListInit(LOS_DL_LIST *list)

LOS_DL_LIST的兩個成員pstPrevpstNext, 是LOS_DL_LIST結構體類型的指針。須要爲雙向鏈表節點申請長度爲sizeof(LOS_DL_LIST)的一段內存空間。爲鏈表節點申請到內存後,能夠調用初始化LOS_ListInit(LOS_DL_LIST *list)方法,把這個節點連接爲環狀的雙向鏈表。初始化鏈表時,只有一個鏈表節點,這個節點的前驅和後繼節點都是自身。鏈表節點初始化爲鏈表,如圖所示:

LOS_ListInit

源碼以下:

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_ListInit(LOS_DL_LIST *list)
{
    list->pstNext = list;
    list->pstPrev = list;
}

2.2 LOS_DL_LIST_HEAD(LOS_DL_LIST list)

除了LOS_ListInit(),還提供了一個相同功能的函數式宏LOS_DL_LIST_HEAD,經過直接定義一個雙向鏈表節點,實現將該節點初始化爲雙向鏈表。區別於LOS_ListInit(),在調用函數式宏前,不須要動態申請內存空間。

#define LOS_DL_LIST_HEAD(list) LOS_DL_LIST list = { &(list), &(list) }

3 判斷空鏈表

3.1 LOS_ListEmpty(LOS_DL_LIST *list)

該內聯函數用於判斷鏈表是否爲空。若是雙向鏈表的前驅/後繼節點均爲自身,只有一個鏈節點,沒有掛載業務結構體的鏈表節點,稱該鏈表爲空鏈表。

源碼以下:

LITE_OS_SEC_ALW_INLINE STATIC_INLINE BOOL LOS_ListEmpty(LOS_DL_LIST *node)
{
    return (BOOL)(node->pstNext == node);
}

4 插入雙向鏈表節點

雙向鏈表提供三種鏈表節點插入方法,在指定鏈表節點後面插入LOS_ListAdd、尾部插入LOS_ListTailInsert、頭部插入LOS_ListHeadInsert。在頭部插入的節點,從頭部開始遍歷時第一個遍歷到,從尾部插入的節點,最後一個遍歷到。

4.1 LOS_ListAdd(LOS_DL_LIST *list, LOS_DL_LIST *node)

該內聯函數往鏈表節點*list所在的雙向鏈表中插入一個鏈表節點*node,插入位置在鏈表節點*list的後面。如圖所示,在插入過程當中,會將*node的後繼節點設置爲list->pstNext*node的前驅節點爲*list,並將list->pstNext的前驅節點從*list修改成*node*list的後繼節點從list->pstNext修改成*node

圖示:

LOS_ListAdd

源碼以下:

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;
}

4.2 LOS_ListTailInsert(LOS_DL_LIST *list, LOS_DL_LIST *node)

該內聯函數往鏈表節點*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);
}

4.3 LOS_ListHeadInsert(LOS_DL_LIST *list, LOS_DL_LIST *node)

該內聯函數和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);
}

5 刪除雙向鏈表節點

雙向鏈表提供兩種鏈表節點的刪除方法,刪除指定節點LOS_ListDelete()、刪除並初始化爲一個新鏈表LOS_ListDelInit()

5.1 LOS_ListDelete(LOS_DL_LIST *node)

該內聯函數將鏈表節點*node從所在的雙向鏈表中刪除。節點刪除後,可能須要主動釋放節點所佔用的內存。如圖所示,刪除節點過程當中,會將*node的後繼節點的前驅改成*node的前驅節點,*node的前驅節點的後繼改成*node的後繼節點,並把*node節點的前驅、後繼節點設置爲null,這樣*node節點就脫離了該雙向鏈表。

圖示:

LOS_ListDelete

源碼以下:

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;
}

5.2 LOS_ListDelInit(LOS_DL_LIST *list)

該內聯函數將鏈表節點*list從所在的雙向鏈表中刪除, 並把刪除後的節點從新初始化爲一個新的雙向鏈表。

LOS_ListDelete()相似,該函數也會將*list的後繼節點的前驅改成*list的前驅,*list的前驅節點的後繼改成*list的後繼,但不一樣的是,由於要從新初始化爲新雙向鏈表,因此這個函數並不會把*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);
}

6 獲取雙向鏈表節點

雙向鏈表還提供獲取鏈表節點、獲取包含鏈表的結構體地址的操做。

6.1 LOS_DL_LIST_LAST(object)

獲取指定鏈表節點的前驅節點。

源碼以下:

#define LOS_DL_LIST_LAST(object) ((object)->pstPrev)

6.2 LOS_DL_LIST_FIRST(object)

獲取指定鏈表節點的後繼節點。

源碼以下:

#define LOS_DL_LIST_FIRST(object) ((object)->pstNext)

7 遍歷雙向循環鏈表節點

雙向循環鏈表提供兩種遍歷雙向鏈表的方法,LOS_DL_LIST_FOR_EACHLOS_DL_LIST_FOR_EACH_SAFE

7.1 LOS_DL_LIST_FOR_EACH(item, list)

該宏定義LOS_DL_LIST_FOR_EACH遍歷雙向鏈表,將每次循環獲取的鏈表節點保存在第一個入參中,第二個入參是要遍歷的雙向鏈表的起始節點。這個宏是個for循環條件,在每次循環中,獲取下一個鏈表節點保存到入參item。業務代碼寫在宏後面的代碼塊{}內。

源碼以下:

#define LOS_DL_LIST_FOR_EACH(item, list) \
    for ((item) = (list)->pstNext; (item) != (list); (item) = (item)->pstNext)

咱們以實例演示如何使用LOS_DL_LIST_FOR_EACH。在kernel\src\los_task.c文件中,UINT32 OsPriqueueSize(UINT32 priority)函數的片斷以下:

STATIC UINT32 OsPriqueueSize(UINT32 priority)
{
    UINT32 itemCnt = 0;
    LOS_DL_LIST *curPQNode = (LOS_DL_LIST *)NULL;

⑴  LOS_DL_LIST_FOR_EACH(curPQNode, &g_losPriorityQueueList[priority]) {
        ++itemCnt;
    }

    return itemCnt;
}

其中⑴處代碼,g_losPriorityQueueList[priority]是要循環遍歷的雙向鏈表,curPQNode指向遍歷過程當中的鏈表節點。

7.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)

8 獲取鏈表節點所在結構體

8.1 LOS_OFF_SET_OF(type, member)

根據結構體類型名稱type和其中的成員變量名稱member,獲取member成員變量相對於結構體type的內存地址偏移量。在鏈表的應用場景上,業務結構體包含雙向鏈表做爲成員,當知道雙向鏈表成員變量的內存地址和相對於業務結構體的偏移時,就能夠進一步獲取業務結構體的內存地址,具體見下面LOS_DL_LIST_ENTRY的宏實現。

源碼以下:

#define LOS_OFF_SET_OF(type, member) ((UINTPTR)&((type *)0)->member)

8.2 LOS_DL_LIST_ENTRY(item, type, member)

函數宏中的三個參數分別爲:業務結構體類型名稱type,做爲結構體成員的雙向鏈表成員變量名稱member,做爲結構體成員的雙向鏈表節點指針item。經過調用該宏函數LOS_DL_LIST_ENTRY便可以獲取雙向鏈表節點所在的業務結構體的內存地址。

源碼以下:

基於雙向鏈表節點的內存地址,和雙向鏈表成員變量在結構體中的地址偏移量,能夠計算出結構體的內存地址。

#define LOS_DL_LIST_ENTRY(item, type, member) \
    ((type *)(VOID *)((CHAR *)(item) - LOS_OFF_SET_OF(type, member)))

9 遍歷包含雙向鏈表的結構體

雙向鏈表提供三個宏定義來遍歷包含雙向鏈表成員的結構體,LOS_DL_LIST_FOR_EACH_ENTRYLOS_DL_LIST_FOR_EACH_ENTRY_SAFELOS_DL_LIST_FOR_EACH_ENTRY_HOOK

9.1 LOS_DL_LIST_FOR_EACH_ENTRY(item, list, type, member)

該宏定義LOS_DL_LIST_FOR_EACH_ENTRY經過遍歷雙向鏈表,在每次循環中獲取包含該雙向鏈表成員的結構體變量並保存在第一個入參中。第二個入參是要遍歷的雙向鏈表的起始節點,第三個入參是要獲取的結構體類型名稱,第四個入參是該結構體中的雙向鏈表成員變量的名稱。這個宏是個for循環條件,業務代碼寫在宏後面的代碼塊{}內。

源碼以下:

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))

9.2 LOS_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))

9.3 LOS_DL_LIST_FOR_EACH_ENTRY_HOOK(item, list, type, member, hook)

該宏定義和LOS_DL_LIST_FOR_EACH_ENTRY的區別就是多了一個入參hookhook表示鉤子函數。在每次遍歷循環中,會調用該鉤子函數,實現用戶任務的定製。

源碼以下:

#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)

小結

掌握鴻蒙輕內核的雙向循環鏈表LOS_DL_LIST這一重要的數據結構,會給進一步學習、分析鴻蒙輕內核源代碼打下了基礎,讓後續的學習更加容易。後續也會陸續推出更多的分享文章,敬請期待,也歡迎你們分享學習、使用鴻蒙輕內核的心得,有任何問題、建議,均可以留言給咱們: gitee.com/openharmony… 。爲了更容易找到鴻蒙輕內核代碼倉,建議訪問 gitee.com/openharmony… ,關注Watch、點贊Star、並Fork到本身帳戶下,謝謝。

做者:zhushangyuan

想了解更多內容,請訪問51CTO和華爲合做共建的鴻蒙社區:https://harmonyos.51cto.com/#kyzg

相關文章
相關標籤/搜索