本文由RT-Thread论坛用户@杰阿阿杰原创发布:https://club.rt-thread.org/as...
关于RT-Thread v4.0.4 版本中解决的使用互斥量导致优先级反转的问题探讨
昨天晚上(2021.10.20),rtt 组织了一场线上发布会,展示了 v4.0.4 版本的一些新特性,以及修复的一些问题。其中,@满鉴霆 老师演讲中讲述的一个关于使用互斥量导致线程优先级反转问题,很有意思。
一、简单介绍互斥量
互斥量是线程间同步的一种方式,又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。(引用自 RTT 文档)
二、互斥量解决了什么问题
2.1 线程优先级反转问题
假设当前有三条线程,分别是 A、B、C,它们的优先级关系是 A > B > C,以及一个公用的内存空间 M。为了保证内存空间内数据的安全性,同一时间段内不能有超过一条线程进行操作。即当 C 正在读取 M 的数据时,A 或 B 不能对 M 做修改。
由于这样的规定,会造成优先级反转问题:
- C 就绪,并获得了 M 的控制权
- A 就绪,优先级比 C 高,CPU 优先处理 A
- A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;C 继续读取 M
- B 就绪,优先级比 C 高,CPU 优先处理 B
- B 任务执行完成并挂起,C 继续读取 M
- C 完成了读取 M 数据的操作,释放了 M 的控制权,轮到 A 对 M 进行修改
通过上面的流程,很明显,我们发现,虽然线程 B 的优先级比线程 A 低,但是却优先执行了,这不符合我们对系统实时性的要求。
2.2 互斥量的解决方法
互斥量使用优先级继承协议,解决了上述的优先级反转问题:
- C 就绪,并获得了 M 的控制权
- A 就绪,优先级比 C 高,CPU 优先处理 A
- A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 A 相等,即此时线程优先级关系是:A = C > B;C 继续读取 M
- C 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,轮到 A 对 M 进行修改,唤醒 A
- A 任务执行完成并挂起;B 在3-4之间已就绪,当时因优先级比 C 低,所以无法得到执行,而此时优先级比 C 高,CPU 优先唤醒处理 B
- B 任务执行完成并挂起,C 继续完成任务
三、互斥量制造了什么问题
3.1 错误地使用了 FIFO flag
当用户需要避免上述线程优先级反转问题时,就需要用到互斥量对线程做同步。互斥量由 IPC 容器管理,因此线程想要获取互斥量时,需要在 IPC 中排队等待。IPC 的排队方式有两种:
- RT_IPC_FLAG_FIFO:先进先出,队列按照先进先出方式排队
- RT_IPC_FLAG_PRIO:优先级等待,队列将按照优先级进行排队,优先级高的等待线程将会插队排在优先级低的等待线程前
FIFO 属于非实时调度方式,所有排队等待的线程不再具有优先级的特性。然而,在创建/初始化(create/init)互斥量时,函数却允许用户使用 RT_IPC_FLAG_FIFO 参数,这会导致如下情形:
- C 就绪,并获得了 M 的控制权
- B 就绪,优先级比 C 高,CPU 优先处理 B
- B 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 B 相等,即此时线程优先级关系是:A > B = C,B 进入 FIFO 队列,并排在第一位;C 继续读取 M
- A 就绪,优先级比 C 高,CPU 优先处理 A
- A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 A 相等,即此时线程优先级关系是:A = C > B,A 进入 FIFO 队列,根据先进先出原则,排在第二位,B 后面;C 继续读取 M
- C 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,根据 FIFO 队列,轮到 B 持有 M 的控制权,唤醒 B
- B 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,根据 FIFO 队列,轮到 A 持有 M 的控制权,唤醒 A
- A 任务执行完成并挂起,B 继续任务
- B 任务执行完成并挂起,C 继续任务
由此我们发现,虽然 A 优先级比 B 高,但是由于 B 比 A 先进入 FIFO 队列,导致 B 比 A 优先得到 M 的控制权,并优先执行,这不符合我们使用互斥量的目的。
同时,由于 A 挂起等待互斥量,因此 B 释放互斥量之前,A 都不会被唤醒(除非超时)。这会使得其他优先级高于 B,低于 A 的线程都会优先于 A 执行。谁能忍?
3.2 正确的使用方式
新版本已修复以上出现的优先级反转问题,在创建/初始化(create/init)互斥量时,忽略用户给出的排队方式(flag),只使用 RT_IPC_FLAG_PRIO。
四、总结
互斥量的诞生就是为了解决优先级反转的问题,但是错误地使用互斥量反而会让情况变得更糟糕。同时,这个 bug 相对隐蔽,不易被察觉,初学者(比如我)容易错误地使用,调试时也不容易复现。因此,修复此 bug 是很重要的。
五、结尾
感谢 RTT 昨晚组织的特性解读会,的抽奖活动,让我终于中了一次奖哈哈哈!虽然是三等奖一条数据线,但这是我用这个抽奖小程序以来第一次中奖,真的很想吐槽那个抽奖小程序……
另外,解读会有回放(虽然我暂时不知道在哪)。