前面几期我们介绍过队列、二进制信号量以及计数信号量。但是在使用二进制信号量的时候会有一种优先级反转问题的出现,简而言之就是低优先级任务因为无法及时释放信号量而导致等待信号量发生的高优先级任务迟迟无法进行。
众所周知,FreeRTOS的各任务的运行顺序是由任务的优先级决定的,优先级高的任务比优先级低的任务先执行。
假设我们有三个任务:任务H,任务M,任务L,分别代表高优先级,中优先级以及低优先级。 任务H和任务M同时被挂起,正在等待某一个事件的发生 , 同时任务H和任务L使用同样的全局资源 (意味着当任务L正在占用全局资源时任务H的执行需要等待任务L执行完释放信号量)
当任务L运行时占用了全局资源。
任务H的事件被触发后由于其高优先级任务H获得CPU的使用权
当任务H需要使用全局资源时,由于任务L还没有使用完全局资源,因此任务H被挂起等待任务L的信号量释放即使用函数xSemaphoreTake(SemaphoreHandler,portMAX_DELAY);等待信号量
此时任务L恢复工作
任务M的事件触发,此时任务L在等待任务M的结束,而然后任务H此时也在等待着任务M的任务结束。因此这段时间的任务M优先级高于任务H,这种现象就是优先级反转。
等任务M执行完,任务L继续执行直到释放信号量,任务H得以继续运行。
因此使用信号量就会导致优先级反转的出现,打破原有的任务运行顺序,这在RTOS系统中应当是尽量避免的。
问题解决
为了解决二进制信号量可能带来的优先级反转现象,FreeRTOS中有一种特殊的二进制信号量——互斥信号量也就是互斥锁。
官方的开发者文档中介绍了互斥锁的存在,互斥锁实际上是一个包含了优先级继承机制的二进制信号量。任务在使用资源时则相当于手持一块令牌,其他没有这块令牌的任务无法就使用资源这就是所谓的互斥。当任务结束使用资源时,也就会返回所对应的令牌。
当两个任务使用相同的信号量时,为了避免前面介绍的优先级反转现象, 于是优先级高的任务会把持有令牌的低优先级任务的优先级提升到和自己一样的情况 ,这样子就可以导致中间出现的中优先级任务抢占CPU资源,使得优先级出现反转。
相当于如图,将之后执行的低优先级任务的有限制强制抬高到与自己同一等级,颇有一种富家少爷为了吃葡萄而包下葡萄园的故事感。
这样子的做法可以有效地防止优先级反转现象的出现,也可以使已经出现的优先级反转得到更快的结束。
注意,互斥锁一定一定一定不能在中断中使用,因为中断无法使用延时函数来阻塞事件。
使用互斥锁
在FreeRTOS中有两种方法创建互斥锁,分别是动态创建和静态创建,我们主要介绍一下动态创建互斥锁。
API文档中关于互斥锁的内容很多,主要是介绍互斥锁以及说明使用互斥锁使用的一些细节。
信号量的释放和获取则和二进制信号量一样,参考二进制信号量文章。
我们来测试一下互斥锁。
测试代码
我们需要三个不同优先级的任务,用来测试优先级反转的情况。
我们分别定义高优先级任务,中优先级任务以及低优先级任务。
启动相关宏定义。先测试二进制信号量
void High_Task(void * pvParameters);//高优先级任务
void Mid_Task(void * pvParameters);//中优先级任务
void Low_Task(void * pvParameters);//低优先级任务
void Scan(void * pvParameters);
#define START_TASK_PRIO 1
TaskHandle_t High_Handler;
TaskHandle_t Start_LED_Handler;
TaskHandle_t Mid_Handler;
TaskHandle_t Low_Handler;
TaskHandle_t Scan_Handler;
xSemaphoreHandle TaskSemaphoer_Handler;
void Start_LED(void * pvParameters)
{
taskENTER_CRITICAL();
TaskSemaphoer_Handler = xSemaphoreCreateBinary();//创建二进制信号量
if(TaskSemaphoer_Handler!=NULL)
{
printf("Semaphore Create Successfullyrn");
}
xTaskCreate((TaskFunction_t )High_Task,//任务函数
(char * )"v",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 2,//优先级2
(TaskHandle_t * )&High_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )Mid_Task,//任务函数
(char * )"Mid_Task",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 1,//优先级1
(TaskHandle_t * )&Mid_Handler);//任务函数句柄
xTaskCreate((TaskFunction_t )Low_Task,//任务函数
(char * )"Low_Task",//任务名称
(configSTACK_DEPTH_TYPE) 128,//堆栈空间128Byte
(void* ) NULL,//无返回
(UBaseType_t ) 0,//优先级0
(TaskHandle_t * )&Low_Handler);//任务函数句柄
vTaskSuspend(Mid_Handler);//挂起中优先级任务
vTaskSuspend(High_Handler);//挂起高优先级任务
taskEXIT_CRITICAL();
vTaskDelete(NULL);
}
void FreeRTOS_Init()
{
xTaskCreate((TaskFunction_t )Start_LED,
(char * )"starttask",
(configSTACK_DEPTH_TYPE) 128,
(void* ) NULL,
(UBaseType_t )START_TASK_PRIO,
(TaskHandle_t * )&Start_LED_Handler);
vTaskStartScheduler();
}
接着我们按照会出现优先级反转的情况编写测试代码。
首先挂起高优先级和中优先级任务。
低优先级任务持续打印运行信息,当运行到5次时,恢复高优先级任务持续打印信息,高优先级任务打印三次后等待低优先级任务发送信号量。
当低优先级任务再执行5次后高恢复中优先级任务,再次执行5次后发送信号量示意高优先级任务继续运行。
中优先级任务执行3次后挂起自身。
复制
void Low_Task(void * pvParameters)//参数为 void * pvParameters
{
int Low_number = 0;
while(1)
{
printf("Low_Task Runing 1111rn");
Low_number++;
if(Low_number == 5)
{
vTaskResume(High_Handler);//恢复高优先级
}
if(Low_number == 10)
{
vTaskResume(Mid_Handler);//恢复中优先级任务
}
if(Low_number == 15)
{
xSemaphoreGive(TaskSemaphoer_Handler);//释放信号量
}
}
}
void Mid_Task(void * pvParameters)//参数为 void * pvParameters
{
int Mid_number = 0;
while(1)
{
printf("Mid_Task Runing 2222rn");
Mid_number++;
if(Mid_number == 3)
{
vTaskSuspend(NULL);//挂起自身
}
}
}
void High_Task(void * pvParameters)
{
int High_number = 0;
while(1)
{
printf("High_Task Runing 3333rn");
High_number++;
if(High_number==3)
{
xSemaphoreTake(TaskSemaphoer_Handler,portMAX_DELAY);//等待低优先级任务释放信号量
}
}
}
可以看见和预想的一样,高优先级的任务被中优先级任务所挤兑。
之后我们把代码中的二进制信号量换成互斥锁。
可以看到,中优先级的任务根本没有办法实现优先级反转跳到高优先级去。
因此善于使用互斥锁,避免优先级反转现象的出现有利于FreeRTOS系统任务调度顺序的正确性,防止出现意外错误。