文章目錄
- UCOS-Ⅲ:信号量
-
- 一、信号量基本概念
- 二、調用API
-
- 2.1 建立信号量函數OSSemCreate()
- 2.2 信号量删除函數OSSemDel()
- 2.3 信号量釋放函數OSSemPost()
- 2.4 信号量擷取函數OSSemPend()
- 三、信号量BUG-優先級反轉
- 四、使用執行個體
UCOS-Ⅲ:信号量
一、信号量基本概念
信号量(Semaphore)是一種實作任務間通信的機制,可以實作任務之間同步或臨界資源的互斥通路 (臨界資源指同一時刻隻能有有限個通路),常用于協助一組互相競争的任務來通路臨界資源。運作機制可以了解為:信号量是一個正值,代表資源的可通路數目,當有任務通路時,這個數目減一,任務通路完成時,任務通路結束,釋放他,讓他加一,信号量為0時,其他任務則不能擷取他,選擇退出或者等待挂起,直到有信号量釋放後,按照優先級來擷取信号量,擷取後就緒,其運作流程大概如下圖:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL5NmaOJza61UMRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL4cTO0QjM1AjM5AzMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
UCOS中信号量是核心對象,通過資料類型OS_SEM 定義,OS_SEM 源于結構體os_sem,UCOS中-Ⅲ的信号量相關的代碼都被放在OS_SEM.C 中,通過設定OS_CFG.H 中的OS_CFG_SEM_EN 為1 使能信号量
/* ----------------------------- SEMAPHORES ---------------------------- */
#define OS_CFG_SEM_EN 1u //使能或禁用多值信号量
#define OS_CFG_SEM_DEL_EN 1u //使能或禁用 OSSemDel() 函數
#define OS_CFG_SEM_PEND_ABORT_EN 1u //使能或禁用 OSSemPendAbort() 函數
#define OS_CFG_SEM_SET_EN 1u //使能或禁用 OSSemSet() 函數
信号量的結構體如下:
struct os_sem { /* Semaphore */
/* ------------------ GENERIC MEMBERS ------------------ */
OS_OBJ_TYPE Type; /* Should be set to OS_OBJ_TYPE_SEM */
CPU_CHAR *NamePtr; /* Pointer to Semaphore Name (NUL terminated ASCII) */
OS_PEND_LIST PendList; /* List of tasks waiting on semaphore */
/* ------------------ SPECIFIC MEMBERS ------------------ */
OS_SEM_CTR Ctr;
CPU_TS TS;
};
第一個變量是**“Type”域**:表明UCOS識别所定義的是一個信号量。其它的核心對象也有“Type”域作為結構體的第一個變量。如果函數要調用一個核心對象,UCOS會檢測所調用的核心對象的資料類型是否對應。例如,如果需要傳遞一個消息隊列OS_Q 給函數,但實際傳遞的是一個信号量OS_SEM,UCOS就會檢測出這是一個無效的參數,并傳回錯誤代号。
第二個指針指向核心對象的名字:每個核心對象都可以被賦予一個名字,名字有ASCII 字元串組成,但必須以空字元結尾。
第三個等待清單PendList:若有多個任務等待信号量,信号量就會将這些任務放入其挂起隊列中。
第四個包含一個信号量計數值:信号量計數值可以定義為8 位,16 位,或32 位,取決于OS_TYPE.H 中的OS_SEM_CTR是如何被定義的,這個值用于判斷信号量可用的資源數目**(此值的上限大于1為多值信号量,隻為0或者1則為二值信号量,UCOS-Ⅲ沒有對這兩個多做區分,全看如何使用)**
第五個CPU_TS時間戳:信号量中包含了一個時間戳變量,存儲了上一次信号量被送出時的時間戳。當信号量被送出時,CPU 的時間戳被讀取并存在信号量的時間戳變量中,當OSSemPend()被調用時就能讀取這個時間戳變量。
使用者代碼也不能直接通路信号量中的變量。必須通過UCOS-III 提供的API來進行通路!
二、調用API
UCOS中信号量的調用API有以下四個API,分别為建立、删除、擷取、釋放信号量
2.1 建立信号量函數OSSemCreate()
OSSemCreate()函數進行建立一個信号量,跟消息隊列的建立差不多,我們知道,其實這裡的“建立信号量”指的就是對核心對象(信号量)的一些初始化。
函數入口:
void OSSemCreate (OS_SEM *p_sem, //多值信号量控制塊指針
CPU_CHAR *p_name, //多值信号量名稱
OS_SEM_CTR cnt, //資源數目或事件是否發生标志
OS_ERR *p_err) //傳回錯誤類型
介紹:
p_sem | 指向信号量變量的指針。 |
---|---|
p_name | 指向信号量變量名字字元串的指針。 |
cnt | 信号量的初始值,用作資源保護的信号量這個值通常跟資源的數量相同,用做标志事件發生的信号量這個值設定為0,标志事情還沒有發生。 |
p_err | 指向傳回錯誤類型的指針。 |
p_err傳回錯誤标志,其具體的傳回值對應的情景如下
錯誤傳回值 | 錯誤類型 |
---|---|
OS_ERR_CREATE_ISR | 在中斷中建立信号量是不被允許的,傳回錯誤。 |
OS_ERR_ILLEGAL_CREATE_RUN_TIME | 在定義OSSafetyCriticalStartFlag 為DEF_TRUE 後就不運作建立任何核心對象。 |
OS_ERR_NAME | 參數p_name 是個空指針。 |
OS_ERR_OBJ_CREATED | 信号量已經被建立(不過函數中并沒有涉及到這個錯誤的代碼) |
OS_ERR_OBJ_PTR_NULL | 參數p_sem 是個空指針。 |
OS_ERR_OBJ_TYPE | 參數p_sem 被初始化為别的核心對象了。 |
OS_ERR_NONE | 無錯誤,繼續執行 |
使用執行個體:
//定義結構體
OS_SEM SemOfKey; //标志KEY1是否被按下的多值信号量
//使用前調用API初始化
任務體
{
/* 建立多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量變量的指針
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )5, //表示現有資源數目
(OS_ERR *)&err); //錯誤類型
}
2.2 信号量删除函數OSSemDel()
OSSemDel()用于删除一個信号量,信号量删除函數是根據信号量結構(信号量句柄)直接删除的,删除之後這個信号量的所有資訊都會被系統清空,而且不能再次使用這個信号量了,但是需要注意的是,如果某個信号量沒有被定義,那也是無法被删除的,如果有任務阻塞在該信号量上,那麼盡量不要删除該信号量。使用之前首先要将OS_CFG_SEM_DEL_EN 這個宏置1,注意調用這個函數後,之前用信号量保護的資源将不再得到保護。
函數入口:
OS_OBJ_QTY OSSemDel (OS_SEM *p_sem, //多值信号量指針
OS_OPT opt, //選項
OS_ERR *p_err) //傳回錯誤類型
參數名稱 | 參數作用 |
---|---|
p_sem | 指向信号量變量的指針。 |
opt | 删除信号量時候的選項,有以下兩個選擇。 |
p_err | 指向傳回錯誤類型的指針,有以下幾種可能。 |
參數選項選擇
選項 | 作用 |
---|---|
OS_OPT_DEL_NO_PEND | 當信号量的等待清單上面沒有相應的任務的時候才删除信号量。 |
OS_OPT_DEL_ALWAYS | 不管信号量的等待清單是否有相應的任務都删除信号量。 |
錯誤類型
錯誤傳回值 | 錯誤類型 |
---|---|
OS_ERR_DEL_ISR | 企圖在中斷中删除信号量。 |
OS_ERR_OBJ_PTR_NULL | 參數p_sem 是空指針。 |
OS_ERR_OBJ_TYPE | 參數p_sem 指向的核心變量類型不是信号量 |
OS_ERR_OPT_INVALID | opt 在給出的選項之外 |
OS_ERR_TASK_WAITING | 在選項opt 是OS_OPT_DEL_NO_PEND 的時候,并且信号量等待清單上有等待的任務。 |
同時該函數有一個傳回值,傳回值的含義為:
删除信号量的時候,會将信号量等待清單上的任務脫離該信号量的等待清單。傳回值表示的就是脫離等待清單的任務個數。
使用執行個體
OS_SEM SemOfKey;; //聲明信号量
/* 删除信号量 sem*/
OSSemDel ((OS_SEM *)&SemOfKey, //指向信号量的指針
OS_OPT_DEL_NO_PEND,
(OS_ERR *)&err); //傳回錯誤類型
2.3 信号量釋放函數OSSemPost()
當信号量有值的時候,任務才能擷取信号量,有兩個方式使信号量有值,一個是在建立的時候進行初始化,将它可用的信号量個數設定一個初始值;如果該信号量用作二值信号量,那麼我們在建立信号量的時候其初始值的範圍是0~1,假如初始值為1個可用的信号量的話,被擷取一次就變得無效了,那就需要我們釋放信号量,uCOS 提供了信号量釋放函數,每調用一次該函數就釋放一個信号量。UCOS可以一直釋放信号量,但如果用作二值信号量的話,一直釋放信号量就達不到同步或者互斥通路的效果,雖然說uCOS 的信号量是允許一直釋放的,但是,信号量的範圍還需我們使用者自己根據需求進行決定,當用作二值信号量的時候,必須確定其可用值在0~1 範圍内;而用作計數信号量的話,其範圍是由使用者根據實際情況來決定的
函數入口:
OS_SEM_CTR OSSemPost (OS_SEM *p_sem, //多值信号量控制塊指針
OS_OPT opt, //選項
OS_ERR *p_err) //傳回錯誤類型
參數名稱 | 參數作用 |
---|---|
p_sem | 指向要送出的信号量的指針 |
opt | 釋出信号時的選項,可能有以下幾個選項 |
p_err | 指向傳回錯誤類型的指針,錯誤的類型如下。(隻列了必要部分) |
選項清單:
選項 | 功能 |
---|---|
OS_OPT_POST_1 | 釋出給信号量等待清單中優先級最高的任務。 |
OS_OPT_POST_ALL | 釋出給信号量等待清單中所有的任務。 |
OS_OPT_POST_NO_SCHED | 送出信号量之後要不要進行任務排程,預設是要進行任務排程的,選擇該選項可能的原因是想繼續運作目前任務,因為釋出信号量可能讓那些等待信号量的任務就緒,這個選項沒有進行任務排程,釋出完信号量目前任務還是繼續運作。當任務想釋出多個信号量,最後同時排程的話也可以用這個選項。可以跟上面兩個選項之一相與做為參數。 |
錯誤值:
OS_ERR_SEM_OVF 信号量計數值已經達到最大範圍了,這次送出會引起信号量計數值溢出。
傳回值:
信号量計數值
使用執行個體:
OS_SEM SemOfKey; //标志KEY1 是否被按下的信号量
OSSemPost((OS_SEM *)&SemOfKey, //釋出SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //釋出給所有等待任務
(OS_ERR *)&err); //傳回錯誤類型
2.4 信号量擷取函數OSSemPend()
當任務擷取了某個信号量的時候,該信号量的可用個數就減一,當它減到0 的時候,任務就無法再擷取了,并且擷取的任務會進入阻塞态(假如使用者指定了阻塞逾時時間的話)。如果某個信号量中目前擁有1 個可用的信号量的話,被擷取一次就變得無效了,那麼此時另外一個任務擷取該信号量的時候,就會無法擷取成功,該任務便會進入阻塞态,阻塞時間由使用者指定。uCOS 支援系統中多個任務擷取同一個信号量,假如信号量中已有多個任務在等待,那麼這些任務會按照優先級順序進行排列,如果信号量在釋放的時候選擇隻釋放給一個任務,那麼在所有等待任務中最高優先級的任務優先獲得信号量,而如果信号量在釋放的時候選擇釋放給所有任務,則所有等待的任務都會擷取到信号量
函數入口:
OS_SEM_CTR OSSemPend (OS_SEM *p_sem, //多值信号量指針
OS_TICK timeout, //等待逾時時間
OS_OPT opt, //選項
CPU_TS *p_ts, //等到信号量時的時間戳
OS_ERR *p_err) //傳回錯誤類型
參數:
參數 | 作用 |
---|---|
p_sem | 指向要擷取的信号量變量的指針。 |
opt | 可能是以下幾個選項之一。 |
timeout | 這個參數是設定的是擷取不到信号量的時候等待的時間。如果這個值為0,表示一直等待下去,如果這個值不為0,則最多等待timeout 個時鐘節拍。 |
p_ts | 指向等待的信号量被删除,等待被強制停止,等待逾時等情況時的時間戳的指針。 |
p_err | 指向傳回錯誤類型的指針,有以下幾種類型。 |
功能選項:
功能 | 作用 |
---|---|
OS_OPT_PEND_BLOCKING | 如果不能即刻獲得信号量,選項表示要繼續等待。 |
OS_OPT_PEND_NON_BLOCKING | 如果不能即刻獲得信号量,選項表示不等待信号量。 |
錯誤類型:
錯誤 | 類型 |
---|---|
OS_ERR_OBJ_DEL | 信号量已經被删除了。 |
OS_ERR_OBJ_PTR_NULL | 輸入的信号量變量指針是空類型。 |
OS_ERR_OBJ_TYPE | p_sem 指向的變量核心對象類型不是信号量。 |
OS_ERR_OPT_INVALID | 參數opt 不符合要求。 |
OS_ERR_PEND_ABORT | 等待過程,其他的任務調用了函數OSSemPendAbort 強制取消等待。 |
OS_ERR_PEND_ISR | 企圖在中斷中等待信号量。 |
OS_ERR_PEND_WOULD_BLOCK | 開始擷取不到信号量,且沒有要求等待。 |
OS_ERR_SCHED_LOCKED | 排程器被鎖住。 |
OS_ERR_STATUS_INVALID | 系統出錯,導緻任務控制塊的元素PendStatus 不在可能的範圍内。 |
OS_ERR_TIMEOUT | 等待逾時。 |
OS_ERR_NONE | 成功擷取 |
傳回值:
信号量計數值
使用執行個體:
ctr = OSSemPend ((OS_SEM *)&SemOfKey, //等待該信号量 SemOfKey
(OS_TICK )0, //下面選擇不等待,該參無效
(OS_OPT )OS_OPT_PEND_NON_BLOCKING,//如果沒信号量可用不等待
(CPU_TS *)0, //不擷取時間戳
(OS_ERR *)&err); //傳回錯誤類型
if(err == OS_ERR_NONE)
{
//擷取成功
}
三、信号量BUG-優先級反轉
優先級反轉是實時系統中的一個常見問題,存在于基于優先級的搶占式核心中,優先級反轉的原理如下:
下圖有三個排程任務L、M、H,任務H 的優先級高于任務M,任務M 的優先級高于任務L
任務L最開始運作的時候擷取信号量,之後任務H開始執行,搶占了任務L,在任務H運作的時候,剛好需要擷取信号量,但此時信号量還在任務L的手裡,于是任務H進入挂起隊列,任務L繼續運作,在任務L沒有釋放信号量的時候,任務M過來搶占L運作,在任務L釋放信号量時,任務H才繼續執行,若任務M 需要執行很長時間,則任務H 會被延遲很長時間才執行,這叫做優先級反轉。
解決方法是臨時提高任務L的優先級,這一内容我們下一節互斥量再分析
四、使用執行個體
功能:信号量管理停車位資源,按鍵2按下釋放信号量,停車位+1,序列槽列印數目,按鍵1按下擷取信号量,停車位-1,序列槽顯示是否擷取成功
啟動任務建立信号量
/* 建立多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量變量的指針
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )5, //表示現有資源數目
(OS_ERR *)&err); //錯誤類型
再建立兩個任務
任務一主體:
/*
*********************************************************************************************************
* KEY1 TASK
*********************************************************************************************************
*/
static void AppTaskKey1 ( void * p_arg )
{
OS_ERR err;
OS_SEM_CTR ctr;
CPU_SR_ALLOC(); //使用到臨界段(在關/開中斷時)時必需該宏,該宏聲明和定義一個局部變
//量,用于儲存關中斷前的 CPU 狀态寄存器 SR(臨界段關中斷隻需儲存SR)
//,開中斷時将該值還原。
uint8_t ucKey1Press = 0;
(void)p_arg;
while (DEF_TRUE) { //任務體
if( Key_Scan ( macKEY1_GPIO_PORT, macKEY1_GPIO_PIN, 1, & ucKey1Press ) ) //如果KEY1被按下
{
ctr = OSSemPend ((OS_SEM *)&SemOfKey, //等待該信号量 SemOfKey
(OS_TICK )0, //下面選擇不等待,該參無效
(OS_OPT )OS_OPT_PEND_NON_BLOCKING,//如果沒信号量可用不等待
(CPU_TS *)0, //不擷取時間戳
(OS_ERR *)&err); //傳回錯誤類型
OS_CRITICAL_ENTER(); //進入臨界段
if ( err == OS_ERR_NONE )
printf ( "\r\nKEY1被按下:成功申請到停車位,剩下%d個停車位。\r\n", ctr );
else if ( err == OS_ERR_PEND_WOULD_BLOCK )
printf ( "\r\nKEY1被按下:不好意思,現在停車場已滿,請等待!\r\n" );
OS_CRITICAL_EXIT();
}
OSTimeDlyHMSM ( 0, 0, 0, 20, OS_OPT_TIME_DLY, & err ); //每20ms掃描一次
}
}
任務二主體:
static void AppTaskKey2 ( void * p_arg )
{
OS_ERR err;
OS_SEM_CTR ctr;
CPU_SR_ALLOC(); //使用到臨界段(在關/開中斷時)時必需該宏,該宏聲明和定義一個局部變
//量,用于儲存關中斷前的 CPU 狀态寄存器 SR(臨界段關中斷隻需儲存SR)
//,開中斷時将該值還原。
uint8_t ucKey2Press = 0;
(void)p_arg;
while (DEF_TRUE) { //任務體
if( Key_Scan ( macKEY2_GPIO_PORT, macKEY2_GPIO_PIN, 1, & ucKey2Press ) ) //如果KEY2被按下
{
ctr = OSSemPost((OS_SEM *)&SemOfKey, //釋出SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //釋出給所有等待任務
(OS_ERR *)&err); //傳回錯誤類型
OS_CRITICAL_ENTER(); //進入臨界段
printf ( "\r\nKEY2被按下:釋放1個停車位,剩下%d個停車位。\r\n", ctr );
OS_CRITICAL_EXIT();
}
OSTimeDlyHMSM ( 0, 0, 0, 20, OS_OPT_TIME_DLY, & err ); //每20ms掃描一次
}
}
序列槽現象: