ucos(十)信号量优先级反转
一、概述
信号量,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的区别:
优先级继承,只有当占有资源的低优先级的任务被阻塞时,才会提高占有资源任务的优先级;而优先级天花板,不论是否发生阻塞,都提升。