鴻蒙輕內核源碼分析:掌握信號量使用差異

摘要:本文帶領大家一起剖析鴻蒙輕內核的信號量模塊的源代碼,包含信號量的結構體、信號量池初始化、信號量創建刪除、申請釋放等。

本文分享自華爲雲社區《鴻蒙輕內核M核源碼分析系列十一 信號量Semaphore》,原文作者: zhushy 。

信號量(Semaphore)是一種實現任務間通信的機制,可以實現任務間同步或共享資源的互斥訪問。一個信號量的數據結構中,通常有一個計數值,用於對有效資源數的計數,表示剩下的可被使用的共享資源數。以同步爲目的的信號量和以互斥爲目的的信號量在使用上存在差異。本文通過分析鴻蒙輕內核信號量模塊的源碼,掌握信號量使用上的差異。本文中所涉及的源碼,以OpenHarmony LiteOS-M內核爲例, 均可以在開源站點https://gitee.com/openharmony/kernel_liteos_m 獲取。

接下來,我們看下信號量的結構體,信號量初始化,信號量常用操作的源代碼。

1、信號量結構體定義和常用宏定義

1.1 信號量結構體定義

在文件kernel\include\los_sem.h定義的信號量控制塊結構體爲LosSemCB,結構體源代碼如下。信號量狀態.semStat取值OS_SEM_UNUSED、OS_SEM_USED,其他成員變量的註釋見註釋部分。

typedef struct {
    UINT16 semStat;      /**< 信號量狀態 */
    UINT16 semCount;     /**< 可用的信號量數量 */
    UINT16 maxSemCount;  /**< 可用的信號量最大數量 */
    UINT16 semID;        /**< 信號量Id */
    LOS_DL_LIST semList; /**< 阻塞在該信號量的任務鏈表 */
} LosSemCB;

1.2 信號量常用宏定義

系統支持創建多少信號量是根據開發板情況使用宏LOSCFG_BASE_IPC_SEM_LIMIT定義的,每一個信號量semId是UINT32類型的,取值爲[0,LOSCFG_BASE_IPC_SEM_LIMIT),表示信號量池中各個的信號量的編號。

⑴處的宏表示二值信號量的最大值爲1,⑵處、⑶處的宏表示信號量未使用、使用狀態值。⑷處根據信號量阻塞任務雙向鏈表中的鏈表節點指針ptr獲取信號量控制塊結構體指針。⑸處從信號量池中獲取指定信號量semId對應的信號量控制塊。

⑴    #define OS_SEM_BINARY_MAX_COUNT     1

⑵    #define OS_SEM_UNUSED               0

⑶    #define OS_SEM_USED                 1

⑷    #define GET_SEM_LIST(ptr) LOS_DL_LIST_ENTRY(ptr, LosSemCB, semList)

⑸    #define GET_SEM(semid) (((LosSemCB *)g_allSem) + (semid))

2、信號量初始化

信號量在內核中默認開啓,用戶可以通過宏LOSCFG_BASE_IPC_SEM進行關閉。開啓信號量的情況下,在系統啓動時,在kernel\src\los_init.c中調用OsSemInit()進行信號量模塊初始化。

下面,我們分析下信號量初始化的代碼。

⑴初始化雙向循環鏈表g_unusedSemList,維護未使用的信號量池。⑵爲信號量池申請內存,如果申請失敗,則返回錯誤。⑶循環每一個信號量進行初始化,爲每一個信號量節點指定索引semID,把.semStat設置爲未使用OS_SEM_UNUSED,並執行⑷把信號量節點插入未使用信號量雙向鏈表g_unusedSemList。

LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)
{
    LosSemCB *semNode = NULL;
    UINT16 index;

⑴  LOS_ListInit(&g_unusedSemList);

    if (LOSCFG_BASE_IPC_SEM_LIMIT == 0) {
        return LOS_ERRNO_SEM_MAXNUM_ZERO;
    }

⑵  g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));
    if (g_allSem == NULL) {
        return LOS_ERRNO_SEM_NO_MEMORY;
    }

⑶  for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) {
        semNode = ((LosSemCB *)g_allSem) + index;
        semNode->semID = index;
        semNode->semStat = OS_SEM_UNUSED;
⑷      LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);
    }
    return LOS_OK;
}

3、信號量常用操作

3.1 信號量創建

我們可以使用函數LOS_SemCreate(UINT16 count, UINT32 *semHandle)來創建計數信號量,使用UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)創建二值信號量,下面通過分析源碼看看如何創建信號量的。

2個函數的傳入參數一樣,需要傳入信號量的數量count,和保存信號量編號的semHandle。計數信號量的最大數量爲OS_SEM_COUNTING_MAX_COUNT,二值信號量的最大數量爲OS_SEM_BINARY_MAX_COUNT。會進一步調用函數OsSemCreate()實現信號量的創建,下文繼續分析。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemCreate(UINT16 count, UINT32 *semHandle)
{
    return OsSemCreate(count, OS_SEM_COUNTING_MAX_COUNT, semHandle);
}

LITE_OS_SEC_TEXT_INIT UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)
{
    return OsSemCreate(count, OS_SEM_BINARY_MAX_COUNT, semHandle);
}

我們看看創建信號量的函數OsSemCreate(),需要3個參數,創建的信號量的數量,最大數量,以及信號量編號。

⑴判斷g_unusedSemList是否爲空,還有可以使用的信號量資源?如果沒有可以使用的信號量,調用函數OsSemInfoGetFullDataHook()做些調測相關的檢測,這個函數需要開啓調測開關,後續系列專門分析。

⑵處如果g_unusedSemList不爲空,則獲取第一個可用的信號量節點,接着從雙向鏈表g_unusedSemList中刪除,然後調用宏GET_SEM_LIST獲取LosSemCB *semCreated
,初始化創建的信號量信息,包含信號量的狀態、信號量數量,信號量最大數量等信息。⑶初始化雙向鏈表&semCreated->semList,阻塞在這個信號量上的任務會掛在這個鏈表上。⑷賦值給輸出參數*semHandle,後續程序使用這個信號量編號對信號量進行其他操作。

LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle)
{
    UINT32 intSave;
    LosSemCB *semCreated = NULL;
    LOS_DL_LIST *unusedSem = NULL;
    UINT32 errNo;
    UINT32 errLine;

    if (semHandle == NULL) {
        return LOS_ERRNO_SEM_PTR_NULL;
    }

    if (count > maxCount) {
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_OVERFLOW);
    }

    intSave = LOS_IntLock();

⑴  if (LOS_ListEmpty(&g_unusedSemList)) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_ALL_BUSY);
    }

⑵  unusedSem = LOS_DL_LIST_FIRST(&(g_unusedSemList));
    LOS_ListDelete(unusedSem);
    semCreated = (GET_SEM_LIST(unusedSem));
    semCreated->semCount = count;
    semCreated->semStat = OS_SEM_USED;
    semCreated->maxSemCount = maxCount;
⑶  LOS_ListInit(&semCreated->semList);
⑷  *semHandle = (UINT32)semCreated->semID;
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_CREATE, semCreated);
    return LOS_OK;

ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.2 信號量刪除

我們可以使用函數LOS_semDelete(UINT32 semHandle)來刪除信號量,下面通過分析源碼看看如何刪除信號量的。

⑴處判斷信號量semHandle是否超過LOSCFG_BASE_IPC_SEM_LIMIT,如果超過則返回錯誤碼。如果信號量編號沒有問題,獲取信號量控制塊LosSemCB *semDeleted。⑵處判斷要刪除的信號量的狀態,如果處於未使用狀態,則跳轉到錯誤標籤ERR_HANDLER:進行處理。⑶如果信號量的阻塞任務列表不爲空,不允許刪除,跳轉到錯誤標籤進行處理。⑷處如果信號量可用刪除,則會把.semStat設置爲未使用OS_SEM_UNUSED,並把信號量節點插入未使用信號量雙向鏈表g_unusedSemList。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemDelete(UINT32 semHandle)
{
    UINT32 intSave;
    LosSemCB *semDeleted = NULL;
    UINT32 errNo;
    UINT32 errLine;

⑴  if (semHandle >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
    }

    semDeleted = GET_SEM(semHandle);
    intSave = LOS_IntLock();
⑵  if (semDeleted->semStat == OS_SEM_UNUSED) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
    }

⑶  if (!LOS_ListEmpty(&semDeleted->semList)) {
        LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_PENDED);
    }

⑷  LOS_ListAdd(&g_unusedSemList, &semDeleted->semList);
    semDeleted->semStat = OS_SEM_UNUSED;
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_DELETE, semDeleted);
    return LOS_OK;
ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.3 信號量申請

我們可以使用函數UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)來請求信號量,需要的2個參數分別是信號量semHandle和等待時間timeout,取值範圍爲[0, LOS_WAIT_FOREVER],單位爲Tick。下面通過分析源碼看看如何請求信號量的。

申請信號量時首先會進行信號量編號、參數的合法性校驗。⑴處代碼表示信號量如果大於配置的最大值,則返回錯誤碼。⑵處獲取要申請的信號量控制塊semPended。⑶處調用函數對信號量控制塊進行校驗,如果信號量未創建,處於中斷處理期間,處於鎖任務調度期間,則返回錯誤碼。⑷處如果校驗不通過,跳轉到ERROR_SEM_PEND:標籤停止信號量的申請。

⑸如果信號量計數大於0,信號量計數減1,返回申請成功的結果。⑹如果信號量計數等於0,並且零等待時間timeout,則返回結果碼LOS_ERRNO_SEM_UNAVAILABLE。⑺如果申請的信號量被全部佔用,需要等待時,把當前任務阻塞的信號量.taskSem標記爲申請的信號量,然後調用函數OsSchedTaskWait(),該函數詳細代碼上文已分析,把當前任務狀態設置爲阻塞狀態,加入信號量的阻塞鏈表.semList。如果不是永久等待LOS_WAIT_FOREVER,還需要更改任務狀態爲OS_TASK_STATUS_PEND_TIME,並且設置waitTimes等待時間。⑻處觸發任務調度進行任務切換,暫時不執行後續代碼。

如果等待時間超時,信號量還不可用,本任務獲取不到信號量時,繼續執行⑼,更改任務狀態,返回錯誤碼。如果信號量可用,執行⑽,本任務獲取到信號量,返回申請成功。

LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)
{
    UINT32 intSave;
    LosSemCB *semPended = NULL;
    UINT32 retErr;
    LosTaskCB *runningTask = NULL;

⑴  if (semHandle >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {
        OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
    }

⑵  semPended = GET_SEM(semHandle);
    intSave = LOS_IntLock();

⑶  retErr = OsSemValidCheck(semPended);
    if (retErr) {
⑷      goto ERROR_SEM_PEND;
    }

⑸  if (semPended->semCount > 0) {
        semPended->semCount--;
        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runningTask);
        return LOS_OK;
    }

⑹  if (!timeout) {
        retErr = LOS_ERRNO_SEM_UNAVAILABLE;
        goto ERROR_SEM_PEND;
    }

⑺  runningTask = (LosTaskCB *)g_losTask.runTask;
    runningTask->taskSem = (VOID *)semPended;
    OsSchedTaskWait(&semPended->semList, timeout);
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runningTask);
⑻  LOS_Schedule();

    intSave = LOS_IntLock();
⑼  if (runningTask->taskStatus & OS_TASK_STATUS_TIMEOUT) {
        runningTask->taskStatus &= (~OS_TASK_STATUS_TIMEOUT);
        retErr = LOS_ERRNO_SEM_TIMEOUT;
        goto ERROR_SEM_PEND;
    }

    LOS_IntRestore(intSave);
⑽  return LOS_OK;

ERROR_SEM_PEND:
    LOS_IntRestore(intSave);
    OS_RETURN_ERROR(retErr);
}

3.4 信號量釋放

我們可以使用函數UINT32 LOS_semPost(UINT32 semHandle)來釋放信號量,下面通過分析源碼看看如何釋放信號量的。

釋放信號量時首先會進行信號量編號、參數的合法性校驗,這些比較簡單,自行閱讀即可。⑴處驗判斷是否信號量溢出。⑵如果信號量的任務阻塞鏈表不爲空,執行⑶從阻塞鏈表中獲取第一個任務,設置.taskSem爲NULL,不再阻塞信號量。執行⑷把獲取到信號量的任務調整其狀態,並加入就行隊列。⑸觸發任務調度進行任務切換。⑹如果信號量的任務阻塞鏈表爲空,則把信號量的計數加1。

LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
{
    UINT32 intSave;
    LosSemCB *semPosted = GET_SEM(semHandle);
    LosTaskCB *resumedTask = NULL;

    if (semHandle >= LOSCFG_BASE_IPC_SEM_LIMIT) {
        return LOS_ERRNO_SEM_INVALID;
    }

    intSave = LOS_IntLock();

    if (semPosted->semStat == OS_SEM_UNUSED) {
        LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
    }

⑴  if (semPosted->maxSemCount == semPosted->semCount) {
        LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_SEM_OVERFLOW);
    }
⑵  if (!LOS_ListEmpty(&semPosted->semList)) {
⑶      resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));
        resumedTask->taskSem = NULL;
⑷      OsSchedTaskWake(resumedTask);

        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_POST, semPosted, resumedTask);
⑸      LOS_Schedule();
    } else {
⑹      semPosted->semCount++;
        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_POST, semPosted, resumedTask);
    }

    return LOS_OK;
}

4、信號量使用總結

4.1 計數信號量、二值信號量和互斥鎖

計數信號量和二值信號量唯一的區別就是信號量的初始數量不一致,二值信號量初始數量只能爲0和1,計數信號量的初始值可以爲0和大於1的整數。

互斥鎖可以理解爲一種特性的二值信號量,在實現實現對臨界資源的獨佔式處理、互斥場景時,沒有本質的區別。比對下二值的結構體,互斥鎖的成員變量.muxCount表示加鎖的次數,信號量的成員變量.semCount表示信號量的計數,含義稍有不同。

4.2 信號量的互斥和同步

信號量可用用於互斥和同步兩種場景,以同步爲目的的信號量和以互斥爲目的的信號量在使用上,有如下不同:

  • 用於互斥的信號量

初始信號量計數值不爲0,表示可用的共享資源個數。在需要使用共享資源前,先獲取信號量,然後使用一個共享資源,使用完畢後釋放信號量。這樣在共享資源被取完,即信號量計數減至0時,其他需要獲取信號量的任務將被阻塞,從而保證了共享資源的互斥訪問。對信號量的申請和釋放,需要成對出現,在同一個任務裏完成申請和釋放。

  • 用於同步的信號量

多任務同時訪問同一份共享資源時,會導致衝突,這時候就需要引入任務同步機制使得各個任務按業務需求一個一個的對共享資源進行有序訪問操作。任務同步的實質就是任務按需進行排隊。

用於同步的信號量,初始信號量計數值爲0。任務1申請信號量而阻塞,直到任務2或者某中斷釋放信號量,任務1才得以進入Ready或Running態,從而達到了任務間的同步。信號量的能不能申請成功,依賴其他任務是否釋放信號量,申請和釋放在不同的任務裏完成。

小結

本文帶領大家一起剖析了鴻蒙輕內核的信號量模塊的源代碼,包含信號量的結構體、信號量池初始化、信號量創建刪除、申請釋放等。感謝閱讀,如有任何問題、建議, 都可以留言給我們: https://gitee.com/openharmony/kernel_liteos_m/issues 。爲了更容易找到鴻蒙輕內核代碼倉,建議訪問 https://gitee.com/openharmony/kernel_liteos_m ,關注Watch、點贊Star、並Fork到自己賬戶下,謝謝。

 

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

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