鴻蒙內(nèi)核源碼分析(信號量篇) | 信號量解決任務同步問題
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
基本概念
信號量(Semaphore) 是一種實現(xiàn)任務間通信的機制,可以實現(xiàn)任務間同步或共享資源的互斥訪問。 一個信號量的數(shù)據(jù)結(jié)構(gòu)中,通常有一個計數(shù)值,用于對有效資源數(shù)的計數(shù),表示剩下的可被使用的共享資源數(shù),其值的含義分兩種情況:
- 0,表示該信號量當前不可獲取,因此可能存在正在等待該信號量的任務。 正值,表示該信號量當前可被獲取。
以同步為目的的信號量和以互斥為目的的信號量在使用上有如下不同:
- 用作互斥時,初始信號量計數(shù)值不為0,表示可用的共享資源個數(shù)。在需要使用共享資源前,先獲取信號量,然后使用一個共享資源,使用完畢后釋放信號量。這樣在共享資源被取完,即信號量計數(shù)減至0時,其他需要獲取信號量的任務將被阻塞,從而保證了共享資源的互斥訪問。另外,當共享資源數(shù)為1時,建議使用二值信號量,一種類似于互斥鎖的機制。
- 用作同步時,初始信號量計數(shù)值為0。任務1獲取信號量而阻塞,直到任務2或者某中斷釋放信號量,任務1才得以進入Ready或Running態(tài),從而達到了任務間的同步。
信號量運作原理
信號量初始化,為配置的N個信號量申請內(nèi)存(N值可以由用戶自行配置,通過 LOSCFG_BASE_IPC_SEM_LIMIT 宏實現(xiàn)),并把所有信號量初始化成未使用,加入到未使用鏈表中供系統(tǒng)使用。
● 信號量創(chuàng)建,從未使用的信號量鏈表中獲取一個信號量,并設(shè)定初值。
● 信號量申請,若其計數(shù)器值大于0,則直接減1返回成功。否則任務阻塞,等待其它任務釋放該信號量, 等待的超時時間可設(shè)定。當任務被一個信號量阻塞時,將該任務掛到信號量等待任務隊列的隊尾。
● 信號量釋放,若沒有任務等待該信號量,則直接將計數(shù)器加1返回。否則喚醒該信號量等待任務隊列上的第一個任務。
● 信號量刪除,將正在使用的信號量置為未使用信號量,并掛回到未使用鏈表。
信號量允許多個任務在同一時刻訪問共享資源,但會限制同一時刻訪問此資源的最大任務數(shù)目。 當訪問資源的任務數(shù)達到該資源允許的最大數(shù)量時,會阻塞其他試圖獲取該資源的任務,直到有任務釋放該信號量。
信號量長什么樣?
- typedef struct {
- UINT8 semStat; /**< Semaphore state *///信號量的狀態(tài)
- UINT16 semCount; /**< Number of available semaphores *///有效信號量的數(shù)量
- UINT16 maxSemCount; /**< Max number of available semaphores *///有效信號量的最大數(shù)量
- UINT32 semID; /**< Semaphore control structure ID *///信號量索引號
- LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信號量的任務隊列,任務通過阻塞節(jié)點掛上去
- } LosSemCB;
semList,這又是一個雙向鏈表, 雙向鏈表是內(nèi)核最重要的結(jié)構(gòu)體, 可前往 鴻蒙內(nèi)核源碼分析(總目錄) 查看雙向鏈表篇, LOS_DL_LIST像狗皮膏藥一樣牢牢的寄生在宿主結(jié)構(gòu)體上semList上掛的是未來所有等待這個信號量的任務.
初始化信號量模塊
- #ifndef LOSCFG_BASE_IPC_SEM_LIMIT
- #define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信號量的最大個數(shù)
- #endif
- LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)//信號量初始化
- {
- LosSemCB *semNode = NULL;
- UINT32 index;
- LOS_ListInit(&g_unusedSemList);//初始
- /* system resident memory, don't free */
- 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;//拿信號控制塊, 可以直接g_allSem[index]來嘛
- semNode->semID = SET_SEM_ID(0, index);//保存ID
- semNode->semStat = OS_SEM_UNUSED;//標記未使用
- LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通過semList把 信號塊掛到空閑鏈表上
- }
- if (OsSemDbgInitHook() != LOS_OK) {
- return LOS_ERRNO_SEM_NO_MEMORY;
- }
- return LOS_OK;
- }
分析如下:
● 初始化創(chuàng)建了信號量池來統(tǒng)一管理信號量, 默認 1024 個信號量
● 信號ID范圍從 [0,1023]
● 未分配使用的信號量都掛到了全局變量 g_unusedSemList 上.
小建議:鴻蒙內(nèi)核其他池(如進程池,任務池)都采用free來命名空閑鏈表,而此處使用unused,命名風格不太嚴謹,有待改善.
創(chuàng)建信號量
- LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle)
- {
- unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//從未使用信號量池中取首個
- LOS_ListDelete(unusedSem);//從空閑鏈表上摘除
- semCreated = GET_SEM_LIST(unusedSem);//通過semList掛到鏈表上的,這里也要通過它把LosSemCB頭查到. 進程,線程等結(jié)構(gòu)體也都是這么干的.
- semCreated->semCount = count;//設(shè)置數(shù)量
- semCreated->semStat = OS_SEM_USED;//設(shè)置可用狀態(tài)
- semCreated->maxSemCount = maxCount;//設(shè)置最大信號數(shù)量
- LOS_ListInit(&semCreated->semList);//初始化鏈表,后續(xù)阻塞任務通過task->pendList掛到semList鏈表上,就知道哪些任務在等它了.
- *semHandle = semCreated->semID;//參數(shù)帶走 semID
- OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count);
- return LOS_OK;
- ERR_HANDLER:
- OS_RETURN_ERROR_P2(errLine, errNo);
- }
分析如下:
● 從未使用的空閑鏈表中拿首個信號量供分配使用.
● 信號量的最大數(shù)量和信號量個數(shù)都由參數(shù)指定.
● 信號量狀態(tài)由 OS_SEM_UNUSED 變成了 OS_SEM_USED
● semHandle帶走信號量ID,外部由此知道成功創(chuàng)建了一個編號為 *semHandle 的信號量
申請信號量
- LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)
- {
- UINT32 intSave;
- LosSemCB *semPended = GET_SEM(semHandle);//通過ID拿到信號體
- UINT32 retErr = LOS_OK;
- LosTaskCB *runTask = NULL;
- if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {
- OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
- }
- if (OS_INT_ACTIVE) {
- PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n");
- OsBackTrace();
- return LOS_ERRNO_SEM_PEND_INTERR;
- }
- runTask = OsCurrTaskGet();//獲取當前任務
- if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) {
- OsBackTrace();
- return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK;
- }
- SCHEDULER_LOCK(intSave);
- if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) {
- retErr = LOS_ERRNO_SEM_INVALID;
- goto OUT;
- }
- /* Update the operate time, no matter the actual Pend success or not */
- OsSemDbgTimeUpdateHook(semHandle);
- if (semPended->semCount > 0) {//還有資源可用,返回肯定得成功,semCount=0時代表沒資源了,task會必須去睡眠了
- semPended->semCount--;//資源少了一個
- goto OUT;//注意這里 retErr = LOS_OK ,所以返回是OK的
- } else if (!timeout) {
- retErr = LOS_ERRNO_SEM_UNAVAILABLE;
- goto OUT;
- }
- if (!OsPreemptableInSched()) {//不能申請調(diào)度 (不能調(diào)度的原因是因為沒有持有調(diào)度任務自旋鎖)
- PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n");
- OsBackTrace();
- retErr = LOS_ERRNO_SEM_PEND_IN_LOCK;
- goto OUT;
- }
- runTask->taskSem = (VOID *)semPended;//標記當前任務在等這個信號量
- retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任務進入等待狀態(tài),當前任務會掛到semList上,并在其中切換任務上下文
- if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:這里是涉及到task切換的,把自己掛起,喚醒其他task
- runTask->taskSem = NULL;
- retErr = LOS_ERRNO_SEM_TIMEOUT;
- }
- OUT:
- SCHEDULER_UNLOCK(intSave);
- return retErr;
- }
分析如下: 這個函數(shù)有點復雜,大量的goto,但別被它繞暈了,盯著返回值看. 先說結(jié)果只有一種情況下申請信號量能成功(即 retErr == LOS_OK)
- if (semPended->semCount > 0) {//還有資源可用,返回肯定得成功,semCount=0時代表沒資源了,task會必須去睡眠了
- semPended->semCount--;//資源少了一個
- goto OUT;//注意這里 retErr = LOS_OK ,所以返回是OK的
- }
其余申請失敗的原因有:
● 信號量ID超出范圍(默認1024)
● 中斷發(fā)生期間
● 系統(tǒng)任務
● 信號量狀態(tài)不對,信號量ID不匹配
以上都是異常的判斷,再說正常情況下 semPended->semCount = 0時的情況,沒有資源了怎么辦? 任務進入 OsTaskWait 睡眠狀態(tài),怎么睡,睡多久,由參數(shù) timeout 定 timeout 值分以下三種模式:
- 無阻塞模式:即任務申請信號量時,入?yún)?timeout 等于0。若當前信號量計數(shù)值不為0,則申請成功,否則立即返回申請失敗。
- 永久阻塞模式:即任務申請信號量時,入?yún)?timeout 等于0xFFFFFFFF。若當前信號量計數(shù)值不為0,則申請成功。 否則該任務進入阻塞態(tài),系統(tǒng)切換到就緒任務中優(yōu)先級最高者繼續(xù)執(zhí)行。任務進入阻塞態(tài)后,直到有其他任務釋放該信號量,阻塞任務才會重新得以執(zhí)行。
- 定時阻塞模式:即任務申請信號量時,0
在 OsTaskWait 中,任務將被掛入semList鏈表,semList上掛的都是等待這個信號量的任務.
釋放信號量
- LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched)
- {
- LosSemCB *semPosted = NULL;
- LosTaskCB *resumedTask = NULL;
- if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) {
- return LOS_ERRNO_SEM_INVALID;
- }
- semPosted = GET_SEM(semHandle);
- if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) {
- return LOS_ERRNO_SEM_INVALID;
- }
- /* Update the operate time, no matter the actual Post success or not */
- OsSemDbgTimeUpdateHook(semHandle);
- if (semPosted->semCount == OS_SEM_COUNT_MAX) {//當前信號資源不能大于最大資源量
- return LOS_ERRNO_SEM_OVERFLOW;
- }
- if (!LOS_ListEmpty(&semPosted->semList)) {//當前有任務掛在semList上,要去喚醒任務
- resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面掛的都是task->pendlist節(jié)點,取第一個task下來喚醒
- resumedTask->taskSem = NULL;//任務不用等信號了,重新變成NULL值
- OsTaskWake(resumedTask);//喚醒任務,注意resumedTask一定不是當前任務,OsTaskWake里面并不會自己切換任務上下文,只是設(shè)置狀態(tài)
- if (needSched != NULL) {//參數(shù)不為空,就返回需要調(diào)度的標簽
- *needSched = TRUE;//TRUE代表需要調(diào)度
- }
- } else {//當前沒有任務掛在semList上,
- semPosted->semCount++;//信號資源多一個
- }
- return LOS_OK;
- }
- LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
- {
- UINT32 intSave;
- UINT32 ret;
- BOOL needSched = FALSE;
- SCHEDULER_LOCK(intSave);
- ret = OsSemPostUnsafe(semHandle, &needSched);
- SCHEDULER_UNLOCK(intSave);
- if (needSched) {//需要調(diào)度的情況
- LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU發(fā)送調(diào)度指令
- LOS_Schedule();////發(fā)起調(diào)度
- }
- return ret;
- }
分析如下:
● 注意看在什么情況下 semPosted->semCount 才會 ++ ,是在LOS_ListEmpty為真的時候,semList是等待這個信號量的任務. semList上的任務是在OsTaskWait中掛入的.都在等這個信號.
● 每次OsSemPost都會喚醒semList鏈表上一個任務,直到semList為空.
● 掌握信號量的核心是理解 LOS_SemPend 和 LOS_SemPost
編程示例
本實例實現(xiàn)如下功能:
● 測試任務Example_TaskEntry創(chuàng)建一個信號量,鎖任務調(diào)度,創(chuàng)建兩個任務Example_SemTask1、Example_SemTask2,Example_SemTask2優(yōu)先級高于Example_SemTask1,兩個任務中申請同一信號量,解鎖任務調(diào)度后兩任務阻塞,測試任務Example_TaskEntry釋放信號量。
● Example_SemTask2得到信號量,被調(diào)度,然后任務休眠20Tick,Example_SemTask2延遲,Example_SemTask1被喚醒。
● Example_SemTask1定時阻塞模式申請信號量,等待時間為10Tick,因信號量仍被Example_SemTask2持有,Example_SemTask1掛起,10Tick后仍未得到信號量, Example_SemTask1被喚醒,試圖以永久阻塞模式申請信號量,Example_SemTask1掛起。
● 20Tick后Example_SemTask2喚醒, 釋放信號量后,Example_SemTask1得到信號量被調(diào)度運行,最后釋放信號量。
● Example_SemTask1執(zhí)行完,40Tick后任務Example_TaskEntry被喚醒,執(zhí)行刪除信號量,刪除兩個任務。
- /* 任務ID */
- static UINT32 g_testTaskId01;
- static UINT32 g_testTaskId02;
- /* 測試任務優(yōu)先級 */
- #define TASK_PRIO_TEST 5
- /* 信號量結(jié)構(gòu)體id */
- static UINT32 g_semId;
- VOID Example_SemTask1(VOID)
- {
- UINT32 ret;
- printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n");
- /* 定時阻塞模式申請信號量,定時時間為10ticks */
- ret = LOS_SemPend(g_semId, 10);
- /*申請到信號量*/
- if (ret == LOS_OK) {
- LOS_SemPost(g_semId);
- return;
- }
- /* 定時時間到,未申請到信號量 */
- if (ret == LOS_ERRNO_SEM_TIMEOUT) {
- printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n");
- /*永久阻塞模式申請信號量*/
- ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
- printf("Example_SemTask1 wait_forever and get sem g_semId .\n");
- if (ret == LOS_OK) {
- LOS_SemPost(g_semId);
- return;
- }
- }
- }
- VOID Example_SemTask2(VOID)
- {
- UINT32 ret;
- printf("Example_SemTask2 try get sem g_semId wait forever.\n");
- /* 永久阻塞模式申請信號量 */
- ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
- if (ret == LOS_OK) {
- printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n");
- }
- /* 任務休眠20 ticks */
- LOS_TaskDelay(20);
- printf("Example_SemTask2 post sem g_semId .\n");
- /* 釋放信號量 */
- LOS_SemPost(g_semId);
- return;
- }
- UINT32 ExampleTaskEntry(VOID)
- {
- UINT32 ret;
- TSK_INIT_PARAM_S task1;
- TSK_INIT_PARAM_S task2;
- /* 創(chuàng)建信號量 */
- LOS_SemCreate(0,&g_semId);
- /* 鎖任務調(diào)度 */
- LOS_TaskLock();
- /*創(chuàng)建任務1*/
- (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
- task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1;
- task1.pcName = "TestTsk1";
- task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
- task1.usTaskPrio = TASK_PRIO_TEST;
- ret = LOS_TaskCreate(&g_testTaskId01, &task1);
- if (ret != LOS_OK) {
- printf("task1 create failed .\n");
- return LOS_NOK;
- }
- /* 創(chuàng)建任務2 */
- (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
- task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2;
- task2.pcName = "TestTsk2";
- task2.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE;
- task2.usTaskPrio = (TASK_PRIO_TEST - 1);
- ret = LOS_TaskCreate(&g_testTaskId02, &task2);
- if (ret != LOS_OK) {
- printf("task2 create failed .\n");
- return LOS_NOK;
- }
- /* 解鎖任務調(diào)度 */
- LOS_TaskUnlock();
- ret = LOS_SemPost(g_semId);
- /* 任務休眠40 ticks */
- LOS_TaskDelay(40);
- /* 刪除信號量 */
- LOS_SemDelete(g_semId);
- /* 刪除任務1 */
- ret = LOS_TaskDelete(g_testTaskId01);
- if (ret != LOS_OK) {
- printf("task1 delete failed .\n");
- return LOS_NOK;
- }
- /* 刪除任務2 */
- ret = LOS_TaskDelete(g_testTaskId02);
- if (ret != LOS_OK) {
- printf("task2 delete failed .\n");
- return LOS_NOK;
- }
- return LOS_OK;
- }
實例運行結(jié)果:
- Example_SemTask2 try get sem g_semId wait forever.
- Example_SemTask1 try get sem g_semId ,timeout 10 ticks.
- Example_SemTask2 get sem g_semId and then delay 20ticks .
- Example_SemTask1 timeout and try get sem g_semId wait forever.
- Example_SemTask2 post sem g_semId .
- Example_SemTask1 wait_forever and get sem g_semId .
參與貢獻
● 訪問注解倉庫地址
● Fork 本倉庫 >> 新建 Feat_xxx 分支 >> 提交代碼注解 >> 新建 Pull Request
● 新建 Issue
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)