1. 程式人生 > 其它 >鴻蒙輕核心原始碼分析:掌握訊號量使用差異

鴻蒙輕核心原始碼分析:掌握訊號量使用差異

摘要:本文帶領大家一起剖析鴻蒙輕核心的訊號量模組的原始碼,包含訊號量的結構體、訊號量池初始化、訊號量建立刪除、申請釋放等。

本文分享自華為雲社群《鴻蒙輕核心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到自己賬戶下,謝謝。

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