本节继续讲freeRTOS的信号量,先讲一个与抢占式调度和信号量有关的经典问题——优先级反转,再讲互斥信号量。
假设这样一种情况:
在一个具有抢占式调度的操作系统中,有三个任务A、B、C,它们优先级从高到低是A>B>C;任务A和C中都会需要获取信号量S。优先级反转产生的例子见下图:
初始0时刻,最低优先级的任务C正在运行,并且获取了信号量S;
之后,在1时刻,高优先级的任务A进入就绪态,由于它优先级最高,抢占了CPU,A开始运行;
到2时刻,任务A执行到需要获取信号量S时,发现信号量已经被占用了,所以被阻塞等待;任务A被阻塞后,C重新开始执行;
到3时刻,中等优先级的任务B进入了就绪态,由于任务B比任务C优先级高,则B抢占了CPU,B进入运行态;
只有当任务B执行完毕,任务C才能得到执行权;任务C释放信号量之后任务A才能执行。
这时,就出现了高优先级的任务A,因为信号量S(或者其他资源)的关系,必须等到中等优先级的任务B执行完了,才能执行的异常现象。这个现象就是优先级反转。如果任务B执行的时间很长,那么就会导致高优先级的A也需要等待很长时间。
优先级反转会造成低优先级的任务比高优先级的任务先执行,导致高优先级的任务实时性变差。
那么,如何解决优先级反转问题呢?一般有两种方法:优先级天花板和优先级继承。
优先级继承:当出现高优先级任务A需要等待低优先级任务C占用的资源时,将C的优先级提高到与A一样(有多个任务等待时,提高到等待它占用资源的任务的最高优先级)。
优先级天花板:当低优先级任务C占用资源时,把C的优先级提高到可能访问该资源的任务的最高优先级(称为该资源优先级天花板)。
这两种方法的核心思想,都是将占用信号量的任务的优先级临时提高,使得它不会被随意抢占;区别是优先级继承只在高优先级任务被阻塞时提高低优先级任务的优先级,而优先级天花板在低优先级任务获取资源时就提高了自己的优先级。
我们可以利用下面两个函数来修改、恢复任务的优先级:
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask ); //获取任务的优先级
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority ); //修改任务的优先级
比如,为了避免优先级反转,我们用优先级天花板方法:C任务取得信号量时,把C任务的优先级提高到与A任务一样;释放信号量时,再把C任务的优先级修改为原来的即可。
freeRTOS提供的互斥信号量,是一种特殊的二值信号量:
首先,它与二值信号量的功能差不多,可以实现资源的互斥访问(A获取了B就不能再获取,只能等到A释放之后其他任务才能获取);
其次,它自带了优先级继承功能,当互斥信号量已经被低优先级任务C获取,如果高优先级任务A需要等待这个信号量,会自动将任务C的优先级提高到与自己相同。
互斥信号量的功能使得它能避免优先级反转问题。
互斥信号量的创建使用下列两个函数:
SemaphoreHandle_t xSemaphoreCreateMutex( void )
SemaphoreHandle_t xSemaphoreCreateMutexStatic( StaticSemaphore_t *pxMutexBuffer )
释放信号量、获取信号量的函数与之前讲过的计数信号量、二值信号量相同,函数内部会自动判断是否是互斥信号量,如果是,它会执行优先级继承相关的调整。
释放信号量:
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //释放信号量
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken); //从中断中释放信号量,后一个参数的返回值表示是否有高优先级的任务已经就绪,如果有,则需要进行一次任务切换
获取信号量:
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait ); //获取信号量,后一个参数为阻塞的超时时间
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore, BaseType_t *pxHigherPriorityTaskWoken); //在中断中获取信号量
下面我们就以两个例子来演示优先级反转,以及使用互斥信号量避免优先级反转问题。
首先看优先级反转的试验:
我们定义三个任务:
DefaultTask优先级最低,Task02优先级中等,Task03优先级最高:
在Defaulttask任务中,在运行的0时刻,定义完二值信号量之后,立即占用信号量100ms,之后再释放:
任务Task02在50ms时,开始执行,它需要执行500ms,与信号量无关:
Task03在100ms时开始运行,它需要等待信号量:
运行结果如下图所示:
可以发现,起始时刻到50ms时,defaulttask占用了信号量之后,被Task02抢占了CPU;
到了100ms,Task03开始后运行,它优先级最高;但是它执行到等待信号量时,由于获取不到,自己进入了阻塞态;
之后由于最高优先级的任务是Task02,它一直占用了500ms的CPU,直到它释放CPU,defaulttask才得到执行;
Defaulttask释放信号量之后,Task03才得到执行。
可见由于信号量的影响,发生了优先级反转,最高优先级的任务A直到中等优先级的任务B执行完才能执行。
互斥信号量的优先级继承功能例子
接下来,我们把上述例子中的二值信号量换成互斥信号量,再来看看执行结果。
由于互斥信号量只有定义时与二值信号量不同,所以上例中的代码只需要修改信号量定义那一行:
然后再看执行结果:
可以看到,这个例子中,Task03执行到等待信号量时,defaulttask直接给出了信号量;
原本优先级最低的defaulttask并没有因为task02需要占用CPU而不能执行,说明它的优先级被临时提高了,起到了优先级继承的作用,互斥信号量解决优先级反转问题是有效的。
好了,本节的内容就到这里了。
如果觉得有用可以关注作者微信号“小白白学电子”,在公众号可以找到代码和资料下载地址: