1. 互斥信号量
互斥信号量是一种特殊的二值信号量,用于控制在两个或多个任务之间访问共享资源。互斥信号量提供一种优先级继承机制,让持有互斥信号量的任务优先级提升到等待这个互斥信号量的任务优先级。与二值信号量主要用于同步不同,互斥信号量主要用于互斥访问。除优先级继承机制以外,二者的区别主要在于信号量被获取后发生的事情。
用于互斥的信号量必须归还。
用于同步的信号量通常在完成同步之后便丢弃,不再归还。
互斥信号量在多任务资源共享上相当于与共享资源关联的令牌。一个任务想要合法地访问资源,必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。令牌持有者在完成资源使用后,必须马上归还(Give)令牌。只有归还了令牌,其他任务才可能成功持有令牌,也才可能安全地访问该共享资源。一个任务除非持有令牌,否则不允许访问共享资源。一个典型的互斥信号量用于资源共享的过程如下。
1.1 访问共享资源需要令牌
任务A和任务B都能访问共享资源,只有获得共享资源令牌——互斥信才能访问共享资源。
1.2 获得令牌的任务能访问共享资源
任务A试图通过xSemaphoreTake()函数获取令牌,因为此时令牌没有被其他任务获有,所以任务A能获得这个令牌,从而开始访问共享资源。
1.3 .令牌仅能被一个任务持有
在任务A获得令牌访问共享资源的过程中,任务B试图通过xSemaphoreTake()函数获取令牌,但是因为这时令牌被任务A持有,所以任务B获取令牌失败,进入阻塞态。
1.4 令牌用完必须归还
任务A持有令牌访问共享资源完成后,必要归还令牌,以便其他任务可以获得令牌。
1.5 令牌归还后可被其他任务获取
任务A归还令牌后,任务B通过xSemaphoreTake()函数获取令牌,解除阻塞,经操作系统调度后对共享资源进行访问。
1.6 再次归还令牌
任务B持有令牌的访问共享资源完成后,归还令牌,重新回到起始状态。
2. 创建互斥信号量
宏xSemaphoreCreateMutex()使用动态内存分配方法来创建互斥信号量,其定义如下。
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
实际用于创建互斥信号量的是xQueueCreateMutex()函数。若互斥信号量创建成功则返回这个信号量的句柄,若创建失败则返回NULL。
还有一个用于静态创建互斥信号量的宏xSemaphoreCreateMutexStatic(),在使用该宏创建互斥信号量时需要由用户分配所需内存。
3. 互斥信号量的释放和获取
因为有优先级继承机制,互斥信号量不能用在中断服务函数中,因此中断版本的信号释放和获取函数不能用于互斥信号量。在任务中使用互斥信号量,其释放和获取与二值信号量完全相同,使用相同的释放和获取函数。
4. 优先级反转
在抢占式内核上使用二值信号量,往往容易出现优先级翻转现象。所谓优先级翻转,是指在任务的事务处理顺序上,高优先级任务的事务处理反而滞后于低优先级任务的事务处理。
假设有3个不同优先级的任务,低优先级和高优先级的任务运行需要获取信号量S,而中优先级的任务运行不需要获取信号量。优先级翻转任务执行过程如下:
在某个时间点,任务H和任务M由于等待某些事件而处于挂起态,任务L开始运行,任务L获取并持有信号量S并继续运行,此时任务H恢复运行,申请获取信号量S,但信号量S被任务L持有,任务H获取信号量失败进入阻塞态,任务L得以继续运行。下一时刻,任务M等待的事件发生,恢复运行,由于任务M的优先级比任务L高,且任务M运行不需要获取信号量S,故任务M可一直运行,直至事务处理完成。任务M事务处理完成后让出CPU使用权,使得任务L得以继续运行,直至任务L的事务处理完成,然后释放信号量S。随后,任务H成功获取信号量S,解除阻塞,直至事务处理完成。
很明显,在这3个任务的运行过程中,高优先级任务H的事务处理被推迟到中优先级任务M及低优先级任务L之后,这就是优先级翻转。
5. 优先级反转示例
本示例通过appStartTask()函数创建4个具有不同优先级的FreeRTOS任务,开启抢占式调度和时间片调度。
- 低优先级任务:优先级为1,任务函数为lowTask(),任务运行需要获取信号量,同时运行信息送往串口。
- 中优先级任务:优先级为2,任务函数为midTask(),任务简单地将运行信息送往串口。
- 高优先级任务:优先级为3,任务函数为highTask(),任务运行需要获取与低优先级任务相同的信号量,同时将运行信息送往串口。
- 串口守护任务:优先级为4,任务函数为printTask(),其功能是将通过队列传送过来的字符信息从串口输出,任何时候只有该守护任务能访问串口。
5.1 任务函数
/**********************************************************************
函 数 名:lowTask
功 能:低优先级任务,任务运行需要获取任务量,并将运行信息发送串口
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 1
**********************************************************************/
static void lowTask(void *pvParameters)
{
uint32_t i =0;
while(1)
{
xSemaphoreTake(Semaphore,portMAX_DELAY); //获取信号量
//生成待打印出信息
sprintf(pcToPrint,"低优先级任务运行 1 \r\n\r\n");
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
for(i=0;i<0x3fffff;i++) //模拟低优先级任务占用信号量处理事务
{
taskYIELD(); //进行任务调度
}
xSemaphoreGive(Semaphore); //释放信号量
vTaskDelay(pdMS_TO_TICKS(1000));//延时1s
}
}
/**********************************************************************
函 数 名:midTask
功 能:中优先级任务,简单将运行信息发送串口
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 2
**********************************************************************/
static void midTask(void *pvParameters)
{
while(1)
{
vTaskDelay(pdMS_TO_TICKS(100));//让低优先级任务先运行
//生成待打印出信息
sprintf(pcToPrint,"中优先级任务运行 2\r\n\r\n");
//打印信息,所有发送串口的信息不能直接输出,通过队列发送给串口守护任务
xQueueSendToBack(xQueuePrint,pcToPrint,0);
vTaskDelay(pdMS_TO_TICKS(1000));//阻塞1s
}
}
/**********************************************************************
函 数 名:printTask
功能说明:串口守护任务使用了一个FreeRTOS队列来对串口实现串行化访问,该守护任务是唯一能够直接访问串口的任务。
串口守护任务大部分时间都在阻塞态等待队列中有消息到来,当一个消息到达时,
串口守护任务仅简单地将接收到的消息发送到串口上,然后又返回阻塞态,继续等待下一条消息的到来。
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 4
**********************************************************************/
void printTask(void *pvParameters)
{
char pcTowrite[80]; //缓存从队列接受到的数据
while(1)
{
/*当队列为空,即没有字符需要输出时间,阻塞超时时间为portMAX_DELAY,任务将进入无期限等待
状态,可以不检测队列读取函数的返回值*/
xQueueReceive(xQueuePrint,pcTowrite,portMAX_DELAY);
printf("%s",pcTowrite);
}
}
/**********************************************************************
函 数 名:highTask
功能说明:高优先级任务,任务运行需要获取信号量,并将信息送往串口
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
优 先 级: 3
**********************************************************************/
static void highTask(void *pvParameters)
{
while(1)
{
vTaskDelay(pdMS_TO_TICKS(50)); //让低优先级先持有信号量
sprintf(pcToPrint,"高优先级任务请求信号量......\r\n\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
xSemaphoreTake(Semaphore,portMAX_DELAY); //获取任务量
sprintf(pcToPrint,"高优先级任务运行 3\r\n\r\n");
xQueueSendToBack(xQueuePrint,pcToPrint,0);
xSemaphoreGive(Semaphore);
vTaskDelay(pdMS_TO_TICKS(1000)); //阻塞1s
}
}
5.2 创建任务
/**********************************************************************
函 数 名:appStartTask
功能说明:任务开始函数,用于创建其他函数并且开启调度器
形 参:pvParameters 是在创建该任务时传递的参数
返 回 值:无
**********************************************************************/
void appStartTask(void)
{
/*创建一个长度为2,队列项大小足够容纳待输出字符的队列*/
xQueuePrint = xQueueCreate(2,sizeof(pcToPrint));
/*创造1个二值信号量*/
Semaphore = xSemaphoreCreateBinary();
if(xQueuePrint && Semaphore )//如果队列信号量创建成功
{
xSemaphoreGive(Semaphore); //释放信号量
taskENTER_CRITICAL(); /*进入临界段,关中断*/
xTaskCreate(lowTask,"lowTask",128,NULL,1,&lowTaskHandle);
xTaskCreate(midTask,"midTask",128,NULL,2,&midTaskHandle);
xTaskCreate(highTask,"highTask",128,NULL,3,&highTaskHandle);
xTaskCreate(printTask,"printTask",128,NULL,4,&printTaskHandle);
taskEXIT_CRITICAL(); /*退出临界段,关中断*/
vTaskStartScheduler();/*开启调度器*/
}
}
5.3 下载测试
由运行结果可以看出,低优先级任务持有信号量先运行,接着高优先级任务申请同一个信号量,因为信号量此时被低优先级任务持有,高优先级任务进入阻塞态。中优先级任务由于不需要使用信号量,所以打断了低优先级任务的运行,在低优先级任务持有信号量期间,一直都是中优先级任务在运行。待低优先级任务事务处理完成,释放信号量之后,高优先级任务才得以运行,从而造成了优先级翻转。
6. 用互斥信号量抑制优先级翻转
互斥信号量有优先级继承机制,能够将持有互斥信号量任务的优先级提升到等待这个
互斥信号量任务的优先级,从而抑制优先级翻转。
本示例仅将优先级翻转示例中的二值信号量换成互斥信号量,其他代码完全相同。
由运行结果可以看出,使用互斥信号量可以明显地抑制优先级翻转现象。即便如此,也不能完全依赖互斥信号量来解决优先级翻转问题,解决该问题最好的办法是在程序设计阶段仔细考虑,从任务划分、优先级配置、信号量使用、资源共享等多方面加以考虑。
7. 递归互斥信号量
在使用互斥信号量时,已经获取了这个互斥信号量的任务不能再次获取这个互斥信号量。递归互斥信号量是一种特殊的互斥信号量,已经获取了递归互斥信号量的任务可以重复获取这个递归互斥信号量,而且没有次数的限制。
同互斥信号量一样,递归互斥信号量也有优先级继承机制,同样不能用在中断服务函数中。其他事项,如创建、释放和获取的操作与互斥信号量完全相同,只是API函数的名字不一样而已。
在使用递归互斥信号量时间,获取和释放的次数要一致,在任务中获取了多少次递归互斥信号量,就要释放多少次。