FreeRTOS-优先级反转问题

本文介绍 FreeRTOS 中互斥量(mutex)的优先级继承特性,以及 FreeRTOS 是如何通过互斥量的优先级继承特性,来最小化优先级反转问题带来的负面影响

1 优先级反转问题:

使用互斥量(mutex)保护共享数据一文中,介绍了互斥量的作用是构建一个用来保护共享数据访问过程的临界区,在这个临界区内,任何时候,只允许一个任务进入这个临界区访问共享数据,该任务没退出临界区前,其它任务无法进入临界区中访问数据。
所以,互斥量可以看作是一把钥匙,所有想进入临界区访问共享数据的任务,都需要先获取到这把钥匙,访问完成以后,归还这把钥匙。因此,即使系统中的多个任务是并行/并发的,但访问临界区却是串行的,如下图所示:

因此,互斥量的一个基本特性是:当一个任务task_a持有互斥量mutex后,其它任务在task_a释放互斥量之前,无法再获得互斥量,调用请求互斥量 api,会使当前任务进入阻塞状态(如果设置了等待时间,后文都按此假设)。

基于该基本特性,考虑一个情景,task_a优先级为 1,当它成功获取到互斥量mutex后,正在访问被保护的数据。此时,系统发生了某个事件(如按键事件),导致另一个优先级为3 的任务task_c进入就绪状态(之前处于阻塞态,在等待事件发生),并开始运行(在task_a已经持有互斥量mutex的情况下抢占了cpu)。
如果此时高优先级的task_c也要获取互斥量mutex(为了安全访问共享数据)会怎样? 由于mutex已经被低优先级任务task_a持有,因此高优先级任务task_c无法获得该互斥量,并进入阻塞状态(如果设置了等待时间参数)。内核之后再次调度低优先级任务task_a,当task_a访问数据结束,释放mutex后,高优先级任务task_c才能恢复运行。
这种状态就是优先级反转状态,即高优先级任务,由于获取不到已经被低优先级任务提前获取的互斥量,导致高优先级任务不得不阻塞,先等待低优先级任务运行,并释放互斥量后,才能运行。如下图所示:

注意:下图是基于mutex只能被一个任务持有的特性来讨论的,并未考虑优先级继承特性!

如上图所示的优先级反转状态,并不是那么严重,至少优先级反转状态的持续时间是可预知的(有上界的),因为临界区的基本要求是尽量小,任务应该尽快退出临界区(释放互斥量)。
因此,虽然低优先级任务持有互斥量后,会导致这期间请求同一互斥量的高优先级任务阻塞,但低优先级任务很快会完成数据访问,并释放持有的互斥量,之后高优先级任务就可以恢复运行。

但更糟糕的情况,发生在此时还存在一个中优先级的任务处于就绪状态
低优先级的任务task_a获得互斥量mutex,之后发生了某个事件导致高优先级任务task_c开始运行,并且task_c也请求获取mutex,由于mutex已经被task_a持有,则task_c进入阻塞态。
此时,如果还存在一个中优先级的task_b处于就绪状态,那么task_c阻塞后,内核会先调度中优先级的就绪任务task_b。只要task_b一直运行,task_a就无法得到调度(因为优先级比task_b低),那么被task_a持有的互斥量mutex就不会被释放,结果就导致等待互斥量mutex的高优先级任务task_c一直阻塞(请求mutex时设置了无限等待),无法运行。并且高优先级任务task_c的阻塞时间是没有上界的,因为不确定何时task_b会进入阻塞态,使得内核可以调度任务task_a。甚至,如果有更多的中优先级任务,那么task_a可能会一直得不到运行,使得task_c也一直得不到运行(因为无法获取到mutex)。如下图所示:

注意:下图是基于mutex只能被一个任务持有的特性来讨论的,并未考虑优先级继承!

2 优先级继承

前文讨论的优先级反转问题,都是只基于互斥量只能被一个任务持有的特性来说明的。这种情况下,可以认为 互斥量就是一个普通的二值信号量

但 FreeRTOS 的 互斥量,本质是 二值信号量 + 优先级继承特性。通过优先级继承特征,FreeRTOS提供的互斥量,可以尽量减少任务反转状态的持续时间(有上界)。以之前的例子来解释优先级继承特性:

当低优先级任务task_a获取到互斥量mutex后,mutex的持有者就是task_a(互斥量的内核数据结构中存在一个xMutexHolder成员,当互斥量被某个任务成功获取到,xMutexHolder就被赋值为该任务的句柄)。此时,发生某个事件使得高优先级任务task_c开始运行,并请求获取mutex
这种情况下,由于mutex已经被持有,task_c无法获取到互斥量,所以task_c会阻塞。并且,内核判断出当前请求mutex的任务task_c的优先级,大于mutex的持有者(task_a)的优先级。因此,内核会临时调整持有者task_a的任务优先级(通过互斥量的xMutexHolder成员,找到持有该互斥量的任务,进而修改该任务优先级),让其优先级升高为task_c的优先级。即继承了task_c的任务优先级,如下图所示:

互斥量只能被一个任务持有,当task_a持有互斥量并完成数据访问后,需要释放互斥量,使其它任务可以获取到该互斥量。当task_a释放互斥量时,内核会发现该互斥量的持有者task_a当前任务优先级(前面被临时提升了)和task_a真实任务优先级不同(任务的内核数据结构中存在一个uxBasePriority,该成员记录着任务的真实优先级),因此内核会恢复task_a的任务优先级。如下图所示:

通过优先级继承特性,FreeRTOS的互斥量可以使得优先级反转状态尽快结束(持续时间有上界),减少优先级反转带来的影响。再次考虑前文存在中级优先级任务的例子:
低优先级的任务task_a获得互斥量mutex,之后发生了某个事件导致高优先级任务task_c开始运行(抢占了task_a),并且task_c也请求获取mutex,由于mutex已经被持有,则task_c进入阻塞态,同时mutex的持有者task_a的优先级被临时升高。此时,即使还存在一个中优先级的task_b处于就绪状态,内核仍旧会先调度task_a(因为继承了task_c的高优先级)。当任务task_a释放mutex时,内核会同时恢复task_a的优先级为低优先级。之后,内核调度高优先级任务task_cmutex可用了),当task_c运行一段时间后,因为等待某个其它事件进入阻塞态,内核就会调用目前就绪的最高优先级任务task_b。该过程如下图所示:

最后,需要注意的是,在内核层面,mutex 只能尽量较少优先级反转的持续时间,无法根本解决优先级反转问题。如果,你的系统不希望发生优先级反转状态(不管持续时间是长还是短),就需要从软件设计本身上解决——不要让两个不同优先级的任务去获取同一个互斥量。