优先级反转
实时操作系统的一个基本要求就是基于优先级的抢占系统。保证优先级高的线程在“第一时间”抢到执行权,是实时系统的第一黄金准则。
但是这种基于优先级抢占的系统,有一个著名的问题需要关注,就是“优先级反转”(Priority Inversion),简单来说,就是有低优先级的线程占据了CPU,妨碍了高优先级线程的执行。“优先级反转”是几乎每一个实时操作系统的噩梦,系统设计上花了很多精力去关注,但依然会出错。现代最出名的例子,就是1997美国宇航局的火星探路车(Mars Pathfinder)了。
所以今天就让我们看看优先级反转是怎样发生的,以及对于QNX这样的基于消息传递的操作系统有什么影响。
什么是优先级反转?
概述:优先级:线程C>线程B>线程A。优先级较低的线程B,通过压制优先级更低的线程A,比高优先级的线程C先执行了。
解释:假如线程A拿到一个资源后加锁,线程C因为也需要这个资源于是挂起等待A执行结束。这一段符合逻辑没问题,但是此时线程B因为优先级比线程A高,直接抢占CPU,线程B执行完后,线程A执行,A解锁释放后,C再执行。这就导致原本优先级较低的线程B,通过压制线程A,比高优先级的线程C先执行了。
下面这个时序图就是一个经典的优先级反转
线程A在一个比较低的优先级上工作, 假设是10吧。然后在时间点T1的时候,线程A锁定了一把互斥锁,并开始操作互斥数据。
这时有个高优线级线程C(比如优先级20)在时间点T2被唤醒,它也也需要操作互斥数据。当它加锁互斥锁时,因为互斥锁在T1被线程A锁掉了,所以线程C放弃CPU进入阻塞状态,而线程A得以占据CPU,继续执行。
事情到这一步还是正确的,虽然优先级10的A线程看上去抢了优先级20的C线程的时间,但因为程序逻辑,C确实需要退出CPU等完成互斥数据操作后,才能获得CPU。
但是,假设我们有个线程B在优先级15上,在T3时间点上醒了过来,因为他比当前执行的线程A优先级高,所以它会立即抢占CPU。而线程A被迫进入READY状态等待。
一直到时间点T4,线程B放弃CPU,这时优先级10的线程A是唯一READY线程,它再次占据CPU继续执行,最后在T5解锁了互斥锁。
在T5,线程A解锁的瞬间,线程C立即获取互斥锁,并在优先级20上等待CPU。因为它比线程A的优先级高,系统立刻调度线程C执行,而线程A再次进入READY状态。
上面这个时序里,线程B从T3到T4占据CPU运行的行为,就是事实上的优先级反转。一个优先级15的线程B,通过压制优线级10的线程A,而事实上导致高优先级线程C无法正确得到CPU。这段时间是不可控的,因为线程B可以长时间占据CPU(即使轮转时间片到时,线程A和B都处于可执行态,但是因为B的优先级高,它依然可以占据CPU),其结果就是高优先级线程C可能长时间无法得到 CPU。
优先级反转的后果
低优先级的任务比高优先级的任务先执行,导致任务的错乱,逻辑错乱;
可能造成系统崩溃;
死锁;优先级低的线程迟迟得不到调度,具有高优先级的线程不能执行,死锁;
如何防止优先级反转的方法
其实也不是很复杂,低优先级的A线程获得互斥锁前,需要先将自己的优先级临时提高,最后处理完后再退回原优先级。
set_priority(20);
pthread_mutex_lock();
….
pthread mutex unlock();
set_priority(10);
这样在T3的时候,线程虽然有15的优先级,但是对于已经提升到20的线程A无法形成压制,A就会继续执行,直到T5,线程A解锁,线程C立即获得互斥锁并在20上运行,线程B因为优先级低依然无法获取CPU。
举例:
-
优先级反转与QNX
-
脉冲的优先级及其继承
-
QNX上性能优化与优先级继承
在QNX系统开发后期,很多人会面临一个系统性能优化的过程。**如果你以为反正就是看看谁性能太差,把它的优先级提一下就好了,那实在是大错特错了。**从上述关于优先级继承的例子可以看出,在QNX上优先级是“牵一发而动全身”的。有可能你改了某个客户端的优线级,这个优先级会通过消息传递一级一级地传递出去;也有可能,你改的是某个服务器线程的优先级,虽然通过pidin你看到它在优先级22上RECEIVE,但其实一旦收到消息,它会立刻调整自己的优先级,所以修改服务器MsgReceive()线程的优先级是没有什么意义的。
正确的做法一般是,先让各系统都按默认优先级运行。然后针对有问题的部份,用 instrument kernel生成kernel trace,通过kenrel trace来分析在相关时间段内,各个进程的优先级情况,然后对关键进程进行优先级调整。不断重复上述步骤,以达到系统最优状态。
使用信号量可能会造成线程优先级反转,且无法避免
QoS (Quality of Service),用来指示某任务或者队列的运行优先级;
1、记录了持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync, 如果系统不知道持有者所在的线程,则无法知道应该提高谁的优先级,也就无法解决反转问题。
2、慎用dispatch_semaphore 做线程同步
dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);
3、dispatch_semaphore 不能避免优先级反转的原因
在调用dispatch_semaphore_wait() 的时候,系统不知道哪个线程会调用 dispatch_semaphore_signal()方法,系统无法知道owner信息,无法调整优先级。dispatch_group 和semaphore类似,在调用enter()方法的时候,无法预知谁会leave(),所以系统也不知道owner信息
面试题
1. 什么是优先级反转?请解释其概念并举例说明。
答:
优先级反转是指一个低优先级的任务持有了一个共享资源的锁,而高优先级的任务等待该资源释放,此时中优先级的任务运行并抢占了低优先级的CPU先执行了,会导致高优先级的任务被“反转”到中优先级任务之后运行。
举例:
假设有三个任务A、B、C,它们的优先级分别为高、中、低。任务C持有一个锁并正在运行,任务A需要该锁才能运行,但任务B抢占了CPU时间。这会导致,原本任务A优先级更高,应该在任务B前执行,却导致任务A必须等待任务B完成后才能运行。
2. 在iOS开发中,如何避免或解决优先级反转问题?
答:
-
- 优先级继承 : 确保持有锁的低优先级任务提升到高优先级任务的级别,直到释放锁,再恢复成原来的优先级。(记录了锁持有者的api都可以自动避免优先级反转,系统会通过提高相关线程的优先级来解决优先级反转的问题,如 dispatch_sync)
-
- 避免使用dispatch_semphore(信号量)做线程同步:dispatch_semaphore 容易造成优先级反转,因为api没有记录是哪个线程持有了信号量,所以有高优先级的线程在等待锁的时候,内核无法知道该提高那个线程的优先级(QoS);
-
- 使用合适的锁机制:选择合适的锁机制(如NSLock、NSRecursiveLock等)和避免长时间持有锁。
-
- 避免锁竞争:减少共享资源的使用和锁的粒度,避免长时间锁竞争。