一、概述
信號量,Semaphore:英[ˈseməfɔː(r)]。
信號量常用於任務的同步,通過該信號,就能夠控制某個任務的執行,這個信號具有計數值,因此,可以稱為計數信號量。
計數信號量可以用於資源管理,允許多個任務獲取信號量訪問共享資源,但會限制任務的最大數目。訪問的任務數達到可支持的最大數目時,會阻塞其他試圖獲取該信號量的任務,直到有任務釋放了信號量。這就是計數型信號量的運作機制,雖然計數信號量允許多個任務訪問同一個資源,但是也有限定,比如某個資源限定只能有3個任務訪問,那么第4個任務訪問的時候,會因為獲取不到信號量而進入阻塞,等到有任務(比如任務1)釋放掉該資源的時候,第4個任務才能獲取到信號量從而進行資源的訪問,其運作的機制具體見下圖。

圖1 計數信號量運作示意圖
二、PV原語

1965年,荷蘭學者Dijkstra提出了利用信號量機制解決進程同步問題,信號量正式成為有效的進程同步工具,現在信號量機制被廣泛的用於單處理機和多處理機系統以及計算機網絡中。
信號量S是一個整數,S大於等於零時代表可供並發進程使用的資源實體數,但S小於零時則表示正在等待使用臨界區的進程數。
Dijkstra同時提出了對信號量操作的PV原語。
P原語操作的動作是:
(1)S減1;
(2)若S減1后仍大於或等於零,則進程繼續執行;
(3)若S減1后小於零,則該進程被阻塞后進入與該信號相對應的隊列中,然后轉進程調度。
V原語操作的動作是:
(1)S加1;
(2)若相加結果大於零,則進程繼續執行;
(3)若相加結果小於或等於零,則從該信號的等待隊列中喚醒一等待進程,然后再返回原進程繼續執行或轉進程調度。
PV操作對於每一個進程來說,都只能進行一次,而且必須成對使用。在PV原語執行期間不允許有中斷的發生。
信號量的P、V操作,P表示申請一個資源,每次P操作使信號量減1,V是釋放一個資源,每次V操作使信號量加1。信號量表示的是當前可用的資源個數,當信號量為負時,申請資源的進程(任務)就只能等待了。所以,信號量是負的多少,就表明有多少個進程(任務)申請了資源但無資源可用,只能處於等待狀態。
除了訪問共享資源外,亦可中斷/任務控制某任務的執行,稱之為“單向同步”。
關於信號量函數接口可以點擊:共享資源保護
三、優先級反轉
1.概述
在實時系統中使用信號量有可能導致一個嚴重的問題——優先級翻轉,詳見《嵌入式實時操作系統UCOSIII》章節13.3.5。
- 優先級翻轉是當一個高優先級任務通過信號量機制訪問共享資源時,該信號量已被一低優先級任務占有,因此造成高優先級任務被許多具有較低優先級任務阻塞,實時性難以得到保證。
- 優先級翻轉在可剝奪內核中是非常常見的,在實時系統中不允許出現這種現象,這樣會破壞任務的預期順序,可能會導致嚴重的后果,如下歷史:
在1997年7月4號火星探路者號(Mars Pathfinder)發射后,在開始搜集氣象數據之后沒幾天,系統(無故)重啟了。 【溫老師猜測,就是高優先級任務無法及時喂狗,導致復位。】 后來,當然,被相關技術人員找到問題根源,就是,這個優先級翻轉所導致的,然后修復了此bug。

高優先級任務無法運行而低優先級任務(任務M、任務L)可以運行的現象稱為“優先級翻轉”。
2、例程
taskH因為消息隊列阻塞等待接收消息,沒有其他任務發消息給taskH,taskM是使用時間標志組阻塞等待,故taskL會優先執行,導致優先級反轉。
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "includes.h"
static EXTI_InitTypeDef EXTI_InitStructure;
static GPIO_InitTypeDef GPIO_InitStructure;
static NVIC_InitTypeDef NVIC_InitStructure;
//任務L控制塊
OS_TCB TaskL_TCB;
void taskL(void *parg);
CPU_STK taskL_stk[128]; //任務L的任務堆棧,大小為128字,也就是512字節
//任務M控制塊
OS_TCB TaskM_TCB;
void taskM(void *parg);
CPU_STK taskM_stk[128]; //任務M的任務堆棧,大小為128字,也就是512字節
//任務H控制塊
OS_TCB TaskH_TCB;
void taskH(void *parg);
CPU_STK taskH_stk[128]; //任務H的任務堆棧,大小為128字,也就是512字節
OS_SEM g_sem; //信號量
OS_Q g_queue; //消息隊列
OS_FLAG_GRP g_flag_grp; //事件標志組
void res(void)
{
volatile uint32_t i=0xF000000;
while(i--);
}
void exti0_init(void)
{
//打開端口A硬件時鍾
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
//打開SYSCFG硬件時鍾
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //引腳配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; //配置模式為輸入
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed; //配置速率為高速
//GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //配置為推挽輸出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //上下拉電阻不使能
GPIO_Init(GPIOA,&GPIO_InitStructure);
//將EXTI0連接到PA0
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);
//配置EXTI0的觸發方式
EXTI_InitStructure.EXTI_Line = EXTI_Line0; //外部中斷線0
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中斷模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿觸發,用於檢測按鍵的按下
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//配置EXTI0的優先級
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //EXTI0的中斷號
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//搶占優先級0x00
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //響應優先級0x02
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能EXTI0的通道
NVIC_Init(&NVIC_InitStructure);
}
//主函數
int main(void)
{
OS_ERR err;
systick_init(); //時鍾初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中斷分組配置
usart_init(9600); //串口初始化
LED_Init(); //LED初始化
exti0_init();
//OS初始化,它是第一個運行的函數,初始化各種的全局變量,例如中斷嵌套計數器、優先級、存儲器
OSInit(&err);
//創建任務L
OSTaskCreate( (OS_TCB *)&TaskL_TCB, //任務控制塊,等同於線程id
(CPU_CHAR *)"TaskL", //任務的名字,名字可以自定義的
(OS_TASK_PTR)taskL, //任務函數,等同於線程函數
(void *)0, //傳遞參數,等同於線程的傳遞參數
(OS_PRIO)9, //任務的優先級9
(CPU_STK *)taskL_stk, //任務堆棧基地址
(CPU_STK_SIZE)128/10, //任務堆棧深度限位,用到這個位置,任務不能再繼續使用
(CPU_STK_SIZE)128, //任務堆棧大小
(OS_MSG_QTY)0, //禁止任務消息隊列
(OS_TICK)0, //默認時間片長度
(void *)0, //不需要補充用戶存儲區
(OS_OPT)OS_OPT_TASK_NONE, //沒有任何選項
&err //返回的錯誤碼
);
if(err!=OS_ERR_NONE)
{
printf("task L create fail\r\n");
while(1);
}
//創建任務M
OSTaskCreate( (OS_TCB *)&TaskM_TCB, //任務控制塊
(CPU_CHAR *)"TaskM", //任務的名字
(OS_TASK_PTR)taskM, //任務函數
(void *)0, //傳遞參數
(OS_PRIO)8, //任務的優先級8
(CPU_STK *)taskM_stk, //任務堆棧基地址
(CPU_STK_SIZE)128/10, //任務堆棧深度限位,用到這個位置,任務不能再繼續使用
(CPU_STK_SIZE)128, //任務堆棧大小
(OS_MSG_QTY)0, //禁止任務消息隊列
(OS_TICK)0, //默認時間片長度
(void *)0, //不需要補充用戶存儲區
(OS_OPT)OS_OPT_TASK_NONE, //沒有任何選項
&err //返回的錯誤碼
);
if(err!=OS_ERR_NONE)
{
printf("task M create fail\r\n");
while(1);
}
//創建任務H
OSTaskCreate( (OS_TCB *)&TaskH_TCB, //任務控制塊
(CPU_CHAR *)"TaskH", //任務的名字
(OS_TASK_PTR)taskH, //任務函數
(void *)0, //傳遞參數
(OS_PRIO)7, //任務的優先級7
(CPU_STK *)taskH_stk, //任務堆棧基地址
(CPU_STK_SIZE)128/10, //任務堆棧深度限位,用到這個位置,任務不能再繼續使用
(CPU_STK_SIZE)128, //任務堆棧大小
(OS_MSG_QTY)0, //禁止任務消息隊列
(OS_TICK)0, //默認時間片長度
(void *)0, //不需要補充用戶存儲區
(OS_OPT)OS_OPT_TASK_NONE, //沒有任何選項
&err //返回的錯誤碼
);
if(err!=OS_ERR_NONE)
{
printf("task H create fail\r\n");
while(1);
}
//創建信號量,初值為1.思考為什么不寫初值為0
OSSemCreate(&g_sem,"g_sem",1,&err);
//創建事件標志組,所有標志位初值為0
OSFlagCreate(&g_flag_grp,"g_flag_grp",0,&err);
//創建消息隊列,支持6條消息,就支持6個消息指針
OSQCreate(&g_queue,"g_queue",6,&err);
//啟動OS,進行任務調度
OSStart(&err);
printf(".......\r\n");
while(1);
}
void taskL(void *parg)
{
OS_ERR err;
printf("taskL is create ok\r\n");
while(1)
{
OSSemPend(&g_sem,0,OS_OPT_PEND_BLOCKING,NULL,&err);
printf("[taskL]:access res begin\r\n");
res();
printf("[taskL]:access res end\r\n");
OSSemPost(&g_sem,OS_OPT_POST_1,&err);
delay_ms(50);
}
}
void taskM(void *parg)
{
OS_ERR err;
OS_FLAGS flags=0;
printf("taskM is create ok\r\n");
while(1)
{
flags=OSFlagPend(&g_flag_grp,0x01,0,OS_OPT_PEND_FLAG_SET_ANY + OS_OPT_PEND_FLAG_CONSUME+OS_OPT_PEND_BLOCKING,NULL,&err);
if(flags & 0x01)
{
printf("[taskM]:key set\r\n");
}
}
}
void taskH(void *parg)
{
OS_ERR err;
OS_MSG_SIZE msg_size;
char *p=NULL;
printf("taskH is create ok\r\n");
while(1)
{
p=OSQPend(&g_queue,0,OS_OPT_PEND_BLOCKING,&msg_size,NULL,&err);
if(p && msg_size)
{
//將得到的數據內容和數據大小進行打印
printf("[taskH]:queue msg[%s],len[%d]\r\n",p,msg_size);
//清空指向消息的內容
memset(p,0,msg_size);
}
OSSemPend(&g_sem,0,OS_OPT_PEND_BLOCKING,NULL,&err);
printf("[taskH]:access res begin\r\n");
res();
printf("[taskH]:access res end\r\n");
OSSemPost(&g_sem,OS_OPT_POST_1,&err);
}
}
//EXTI0的中斷服務函數
void EXTI0_IRQHandler(void)
{
uint32_t b=0;
OS_ERR err;
OSIntEnter();
//檢測EXTI0是否有中斷請求
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
b=1;
EXTI_ClearITPendingBit(EXTI_Line0);
}
OSIntExit();
if(b)
{
//對事件標志組的bit0置位(1)
OSFlagPost(&g_flag_grp,0x01,OS_OPT_POST_FLAG_SET,&err);
}
}
3、解決方案
為了避免優先級翻轉這個問題,UCOSIII支持一種特殊的二進制信號量:互斥信號量,即互斥鎖,用它可以解決優先級翻轉問題。
目前解決優先級翻轉有許多種方法。其中普遍使用的有2種方法:一種被稱作優先級繼承(priority inheritance);另一種被稱作優先級天花板(priority ceilings)。
- 優先級繼承(priority inheritance) :優先級繼承是指將低優先級任務的優先級提升到等待它所占有的資源的最高優先級任務的優先級。當高優先級任務由於等待資源而被阻塞時,此時資源的擁有者的優先級將會臨時自動被提升,以使該任務不被其他任務所打斷,從而能盡快的使用完共享資源並釋放,再恢復該任務原來的優先級別。
- 優先級天花板(priority ceilings): 優先級天花板是指將申請某資源的任務的優先級提升到可能訪問該資源的所有任務中最高優先級任務的優先級。(這個優先級稱為該資源的優先級天花板) 。這種方法簡單易行, 不必進行復雜的判斷, 不管任務是否阻塞了高優先級任務的運行, 只要任務訪問共享資源都會提升任務的優先級。
A和B的區別:
優先級繼承,只有當占有資源的低優先級的任務被阻塞時,才會提高占有資源任務的優先級;而優先級天花板,不論是否發生阻塞,都提升。