微吼云上線多路互動直播服務 加速多場景互動直播落地
1076
2025-04-01
LiteOS內核源碼分析系列八 信號量Semaphore
信號量(Semaphore)是一種實現任務間通信的機制,可以實現任務間同步或共享資源的互斥訪問。一個信號量的數據結構中,通常有一個計數值,用于對有效資源數的計數,表示剩下的可被使用的共享資源數。以同步為目的的信號量和以互斥為目的的信號量在使用上有如下不同。本文通過分析LiteOS信號量模塊的源碼,掌握信號量使用上的差異。
LiteOS信號量模塊的源代碼,均可以在LiteOS開源站點https://gitee.com/LiteOS/LiteOS 獲取。信號量源代碼、開發文檔,示例程序代碼如下:
LiteOS內核信號量源代碼
包括信號量的私有頭文件kernel\base\include\los_sem_pri.h、頭文件kernel\include\los_sem.h、C源代碼文件kernel\base\los_sem.c。
開發指南文檔–信號量
在線文檔https://gitee.com/LiteOS/LiteOS/blob/master/doc/LiteOS_Kernel_Developer_Guide.md#%E4%BF%A1%E5%8F%B7%E9%87%8F。
接下來,我們看下信號量的結構體,信號量初始化,信號量常用操作的源代碼。
1、信號量結構體定義和常用宏定義
1.1 信號量結構體定義
在文件kernel\base\include\los_sem_pri.h定義的信號量控制塊結構體為LosSemCB,結構體源代碼如下。信號量狀態.semStat取值OS_SEM_UNUSED、OS_SEM_USED,信號量類型.semType取值OS_SEM_COUNTING、OS_SEM_BINARY,分別表示計數信號量和二值信號量。計數信號量的最大數量為OS_SEM_COUNT_MAX,二值信號量的最大數量為OS_SEM_BINARY_COUNT_MAX。
typedef struct { UINT8 semStat; /**< 信號量狀態 */ UINT8 semType; /**< 信號量類型 */ UINT16 semCount; /**< 可用的信號量數量 */ UINT32 semId; /**< 信號量Id,高低16位分別為COUNT(UINT16)|INDEX(UINT16) */ LOS_DL_LIST semList; /**< 阻塞在該信號量的任務鏈表 */ } LosSemCB;
1.2 信號量常用宏定義
系統支持創建多少信號量是根據開發板情況使用宏LOSCFG_BASE_IPC_SEM_LIMIT定義的,每一個信號量semId是UINT32類型的。信號量支持2種方式存取信號量semId,由LOSCFG_RESOURCE_ID_NOT_USE_HIGH_BITS來區分,默認不開啟這個宏,開啟宏的情況比較簡單,自行閱讀代碼。開啟這個宏時,信號量semId由2部分組成:count和index,分別處于高16位和低16位。創建信號量,使用后刪除時,信號量回收到信號量池時,信號量semId的高16位即count值會加1,這樣可以用來表示該信號量被創建刪除的次數。index取值為[0,LOSCFG_BASE_IPC_SEM_LIMIT),表示信號量池中各個的信號量的編號。
⑴處的宏用來分割count和semId的位數,⑵處信號量被刪除時更新信號量semId,可以看出高16位為count和低16位為index。⑶處獲取信號量semId的低16位。⑷根據信號量semId獲取對應的信號量被創建刪除的次數count。⑸處從信號量池中獲取指定信號量semId對應的信號量控制塊。
⑴ #define SEM_SPLIT_BIT 16 ⑵ #define SET_SEM_ID(count, index) (((count) << SEM_SPLIT_BIT) | (index)) ⑶ #define GET_SEM_INDEX(semId) ((semId) & ((1U << SEM_SPLIT_BIT) - 1)) ⑷ #define GET_SEM_COUNT(semId) ((semId) >> SEM_SPLIT_BIT) ⑸ #define GET_SEM(semId) (((LosSemCB *)g_allSem) + GET_SEM_INDEX(semId))
2、信號量初始化
信號量在內核中默認開啟,用戶可以通過宏LOSCFG_BASE_IPC_SEM進行關閉。開啟信號量的情況下,在系統啟動時,在kernel\init\los_init.c中調用OsSemInit()進行信號量模塊初始化。
下面,我們分析下信號量初始化的代碼。
⑴為信號量申請內存,如果申請失敗,則返回錯誤。⑵初始化雙向循環鏈表g_unusedSemList,維護未使用的信號量。
⑶循環每一個信號量進行初始化,為每一個信號量節點指定索引semId。⑷處調用函數OsSemNodeRecycle(),把.semStat設置為未使用OS_SEM_UNUSED,并把信號量節點插入未使用信號量雙向鏈表g_unusedSemList。⑸如果開啟了信號量調測開關,則調用函數OsSemDbgInitHook()進行初始化。
LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID) { LosSemCB *semNode = NULL; UINT16 index; // support at most 65536 semaphores ⑴ g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB))); if (g_allSem == NULL) { return LOS_ERRNO_SEM_NO_MEMORY; } ⑵ LOS_ListInit(&g_unusedSemList); ⑶ for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) { semNode = ((LosSemCB *)g_allSem) + index; semNode->semId = (UINT32)index; ⑷ OsSemNodeRecycle(semNode); } ⑸ if (OsSemDbgInitHook() != LOS_OK) { return LOS_ERRNO_SEM_NO_MEMORY; } return LOS_OK; }
3、信號量常用操作
3.1 信號量創建
我們可以使用函數LOS_SemCreate(UINT16 count, UINT32 *semHandle)來創建計數信號量,使用UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)創建二值信號量,下面通過分析源碼看看如何創建信號量的。
2個函數的傳入參數一樣,需要傳入信號量的數量count,和保存信號量編號的semHandle。計數信號量的數量不能大于OS_SEM_COUNT_MAX,二值信號量的數量不能大于OS_SEM_BINARY_COUNT_MAX。會進一步調用函數OsSemCreate()實現信號量的創建,下文繼續分析。
LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemCreate(UINT16 count, UINT32 *semHandle) { if (count > OS_SEM_COUNT_MAX) { return LOS_ERRNO_SEM_OVERFLOW; } return OsSemCreate(count, OS_SEM_COUNTING, semHandle); } LITE_OS_SEC_TEXT_INIT UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle) { if (count > OS_SEM_BINARY_COUNT_MAX) { return LOS_ERRNO_SEM_OVERFLOW; } return OsSemCreate(count, OS_SEM_BINARY, semHandle); }
我們看看創建信號量的函數OsSemCreate(),需要3個參數,多了個信號量的類型type,區分計數信號量和二值信號量。
⑴判斷g_unusedSemList是否為空,還有可以使用的信號量資源?如果沒有可以使用的信號量,調用函數OsSemInfoGetFullDataHook()做些調測相關的檢測,這個函數需要開啟調測開關,后續系列專門分析。
⑵處如果g_unusedSemList不為空,則獲取第一個可用的信號量節點,接著從雙向鏈表g_unusedSemList中刪除,然后調用宏GET_SEM_LIST獲取LosSemCB *semCreated
,初始化創建的信號量信息,包含信號量的狀態、信號量類型,信號量計數等信息。⑶初始化雙向鏈表&semCreated->semList,阻塞在這個信號量上的任務會掛在這個鏈表上。⑷賦值給輸出參數*semHandle,后續程序使用這個信號量編號對信號量進行其他操作。⑸開啟調測時,會調用函數OsSemDbgUpdateHook()更新信號量的使用情況。
LITE_OS_SEC_TEXT_INIT STATIC UINT32 OsSemCreate(UINT16 count, UINT8 type, UINT32 *semHandle) { UINT32 intSave; LosSemCB *semCreated = NULL; LOS_DL_LIST *unusedSem = NULL; if (semHandle == NULL) { return LOS_ERRNO_SEM_PTR_NULL; } SCHEDULER_LOCK(intSave); ⑴ if (LOS_ListEmpty(&g_unusedSemList)) { SCHEDULER_UNLOCK(intSave); OsSemInfoGetFullDataHook(); OS_RETURN_ERROR(LOS_ERRNO_SEM_ALL_BUSY); } ⑵ unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList); LOS_ListDelete(unusedSem); semCreated = GET_SEM_LIST(unusedSem); semCreated->semStat = OS_SEM_USED; semCreated->semType = type; semCreated->semCount = count; ⑶ LOS_ListInit(&semCreated->semList); ⑷ *semHandle = semCreated->semId; ⑸ OsSemDbgUpdateHook(semCreated->semId, OsCurrTaskGet()->taskEntry, count); SCHEDULER_UNLOCK(intSave); LOS_TRACE(SEM_CREATE, semCreated->semId, type, count); return LOS_OK; }
3.2 信號量刪除
我們可以使用函數LOS_semDelete(UINT32 semHandle)來刪除信號量,下面通過分析源碼看看如何刪除信號量的。
⑴處調用函數OsSemGetCBWithCheck()判斷信號量semHandle是否超過LOSCFG_BASE_IPC_SEM_LIMIT,如果超過則返回錯誤碼。如果信號量編號沒有問題,獲取信號量控制塊LosSemCB *semDeleted。⑵處調用函數OsSemStateVerify()判斷要刪除的信號量Id是否有問題,或者要刪除的信號量處于未使用狀態,則跳轉到錯誤標簽OUT:進行處理。⑶如果信號量的阻塞任務列表不為空,不允許刪除,跳轉到錯誤標簽進行處理。⑷處調用函數OsSemNodeRecycle(),把.semStat設置為未使用OS_SEM_UNUSED,并把信號量節點插入未使用信號量雙向鏈表g_unusedSemList。⑸如果開啟了信號量調測開關,則調用函數OsSemDbgUpdateHook()刷新。
LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemDelete(UINT32 semHandle) { UINT32 intSave; LosSemCB *semDeleted = NULL; UINT32 ret; ⑴ ret = OsSemGetCBWithCheck(semHandle, &semDeleted); if (ret != LOS_OK) { return ret; } SCHEDULER_LOCK(intSave); ⑵ ret = OsSemStateVerify(semHandle, semDeleted); if (ret != LOS_OK) { goto OUT; } ⑶ if (!LOS_ListEmpty(&semDeleted->semList)) { ret = LOS_ERRNO_SEM_PENDED; goto OUT; } #ifndef LOSCFG_RESOURCE_ID_NOT_USE_HIGH_BITS semDeleted->semId = SET_SEM_ID(GET_SEM_COUNT(semDeleted->semId) + 1, GET_SEM_INDEX(semDeleted->semId)); #endif ⑷ OsSemNodeRecycle(semDeleted); ⑸ OsSemDbgUpdateHook(semDeleted->semId, NULL, 0); OUT: SCHEDULER_UNLOCK(intSave); LOS_TRACE(SEM_DELETE, semHandle, ret); return ret; }
3.3 信號量申請
我們可以使用函數UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)來請求信號量,需要的2個參數分別是信號量semHandle和等待時間timeout,取值范圍為[0, LOS_WAIT_FOREVER],單位為Tick。
下面通過分析源碼看看如何請求信號量的。
申請信號量時首先會進行信號量編號、參數的合法性校驗,這些比較簡單。⑴處代碼表示不能在中斷處理時申請信號量。⑵判斷申請信號量的是否系統任務,如果是系統任務,則輸出警告信息。⑶處表示不能在系統鎖調度期間申請信號量。⑷判斷信號量的狀態,未創建的信號量不能申請。
⑸如果信號量計數大于0,信號量計數減1,返回申請成功的結果。⑹如果信號量計數等于0,并且零等待時間timeout,則返回結果碼LOS_ERRNO_SEM_UNAVAILABLE。⑺如果申請的信號量被全部占用,需要等待時,把當前任務阻塞的信號量.taskSem標記為申請的信號量,然后調用函數OsTaskWait(),把當前任務狀態設置為阻塞狀態,加入信號量的阻塞鏈表.semList。如果不是永久等待LOS_WAIT_FOREVER,還需要把當前任務加入定時器排序鏈表。⑻處觸發任務調度進行任務切換,暫時不執行后續代碼。
如果等待時間超時,信號量還不可用,本任務獲取不到信號量時,繼續執行⑼,返回錯誤碼。如果信號量可用,本任務獲取到信號量,返回申請成功。
LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) { UINT32 intSave; LosSemCB *semPended = NULL; UINT32 ret; LosTaskCB *runTask = NULL; ret = OsSemGetCBWithCheck(semHandle, &semPended); if (ret != LOS_OK) { return ret; } LOS_TRACE(SEM_PEND, semHandle, semPended->semCount, timeout); ⑴ if (OS_INT_ACTIVE) { return LOS_ERRNO_SEM_PEND_INTERR; } runTask = OsCurrTaskGet(); ⑵ if (runTask->taskFlags & OS_TASK_FLAG_SYSTEM) { PRINT_DEBUG("Warning: DO NOT recommend to use %s in system tasks.\n", __FUNCTION__); } ⑶ if (!OsPreemptable()) { return LOS_ERRNO_SEM_PEND_IN_LOCK; } SCHEDULER_LOCK(intSave); ⑷ ret = OsSemStateVerify(semHandle, semPended); if (ret != LOS_OK) { goto OUT; } OsSemDbgTimeUpdateHook(semHandle); ⑸ if (semPended->semCount > 0) { semPended->semCount--; goto OUT; ⑹ } else if (!timeout) { ret = LOS_ERRNO_SEM_UNAVAILABLE; goto OUT; } ⑺ runTask->taskSem = (VOID *)semPended; OsTaskWait(&semPended->semList, OS_TASK_STATUS_PEND, timeout); ⑻ OsSchedResched(); SCHEDULER_UNLOCK(intSave); SCHEDULER_LOCK(intSave); ⑼ if (runTask->taskStatus & OS_TASK_STATUS_TIMEOUT) { runTask->taskStatus &= ~OS_TASK_STATUS_TIMEOUT; ret = LOS_ERRNO_SEM_TIMEOUT; goto OUT; } OUT: SCHEDULER_UNLOCK(intSave); return ret; }
3.4 信號量釋放
我們可以使用函數UINT32 LOS_semPost(UINT32 semHandle)來釋放信號量,下面通過分析源碼看看如何釋放信號量的。
釋放信號量時首先會進行信號量編號、參數的合法性校驗,這些比較簡單,自行閱讀即可。⑴處根據信號量類型,獲取信號量計數的最大值maxCount,并進行校驗判斷是否信號量溢出。⑵如果信號量的任務阻塞鏈表不為空,執行⑶從阻塞鏈表中獲取第一個任務,設置.taskSem為NULL,不再阻塞信號量。執行⑷把獲取到信號量的任務調整其狀態,并加入就行隊列。⑸觸發任務調度進行任務切換。⑹如果信號量的任務阻塞鏈表為空,則把信號量的計數加1。
LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle) { UINT32 intSave; LosSemCB *semPosted = NULL; LosTaskCB *resumedTask = NULL; UINT16 maxCount; UINT32 ret; ret = OsSemGetCBWithCheck(semHandle, &semPosted); if (ret != LOS_OK) { return ret; } LOS_TRACE(SEM_POST, semHandle, semPosted->semType, semPosted->semCount); SCHEDULER_LOCK(intSave); ret = OsSemStateVerify(semHandle, semPosted); if (ret != LOS_OK) { goto OUT; } OsSemDbgTimeUpdateHook(semHandle); ⑴ maxCount = (semPosted->semType == OS_SEM_COUNTING) ? OS_SEM_COUNT_MAX : OS_SEM_BINARY_COUNT_MAX; if (semPosted->semCount >= maxCount) { ret = LOS_ERRNO_SEM_OVERFLOW; goto OUT; } ⑵ if (!LOS_ListEmpty(&semPosted->semList)) { ⑶ resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList))); resumedTask->taskSem = NULL; ⑷ OsTaskWake(resumedTask, OS_TASK_STATUS_PEND); SCHEDULER_UNLOCK(intSave); ⑸ LOS_MpSchedule(OS_MP_CPU_ALL); LOS_Schedule(); return LOS_OK; } else { ⑹ semPosted->semCount++; } OUT: SCHEDULER_UNLOCK(intSave); return ret; }
4、信號量使用總結
4.1 計數信號量、二值信號量和互斥鎖
計數信號量和二值信號量唯一的區別就是信號量的初始數量不一致,二值信號量初始數量只能為0和1,計數信號量的初始值可以為0和大于1的整數。
互斥鎖可以理解為一種特性的二值信號量,在實現實現對臨界資源的獨占式處理、互斥場景時,沒有本質的區別。比對下二值的結構體,互斥鎖的成員變量.muxCount表示加鎖的次數,信號量的成員變量.semCount表示信號量的計數,含義稍有不同。
4.2 信號量的互斥和同步
信號量可用用于互斥和同步兩種場景,以同步為目的的信號量和以互斥為目的的信號量在使用上,有如下不同:
用于互斥的信號量
初始信號量計數值不為0,表示可用的共享資源個數。在需要使用共享資源前,先獲取信號量,然后使用一個共享資源,使用完畢后釋放信號量。這樣在共享資源被取完,即信號量計數減至0時,其他需要獲取信號量的任務將被阻塞,從而保證了共享資源的互斥訪問。對信號量的申請和釋放,需要成對出現,在同一個任務里完成申請和釋放。
用于同步的信號量
多任務同時訪問同一份共享資源時,會導致沖突,這時候就需要引入任務同步機制使得各個任務按業務需求一個一個的對共享資源進行有序訪問操作。任務同步的實質就是任務按需進行排隊。
用于同步的信號量,初始信號量計數值為0。任務1申請信號量而阻塞,直到任務2或者某中斷釋放信號量,任務1才得以進入Ready或Running態,從而達到了任務間的同步。信號量的能不能申請成功,依賴其他任務是否釋放信號量,申請和釋放在不同的任務里完成。
小結
本文帶領大家一起剖析了LiteOS信號量模塊的源代碼,包含信號量的結構體、信號量池初始化、信號量創建刪除、申請釋放等。感謝閱讀,如有任何問題、建議,都可以留言給我們: https://gitee.com/LiteOS/LiteOS/issues 。為了更容易找到LiteOS代碼倉,建議訪問 https://gitee.com/LiteOS/LiteOS ,關注Watch、Star、并Fork到自己賬戶下,如下圖,謝謝。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。