【讨论】关于v4.0.4版本中解决的使用互斥量导致优先级反转的问题探讨

本文由RT-Thread论坛用户@杰阿阿杰原创发布:https://club.rt-thread.org/ask/article/3111.html

关于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 做修改。

​ 由于这样的规定,会造成优先级反转问题:

61fa16d75610ca418c881a704571fa29.jpg.webp

  1. C 就绪,并获得了 M 的控制权
  2. A 就绪,优先级比 C 高,CPU 优先处理 A
  3. A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;C 继续读取 M
  4. B 就绪,优先级比 C 高,CPU 优先处理 B
  5. B 任务执行完成并挂起,C 继续读取 M
  6. C 完成了读取 M 数据的操作,释放了 M 的控制权,轮到 A 对 M 进行修改

​ 通过上面的流程,很明显,我们发现,虽然线程 B 的优先级比线程 A 低,但是却优先执行了,这不符合我们对系统实时性的要求。

2.2 互斥量的解决方法

​ 互斥量使用优先级继承协议,解决了上述的优先级反转问题:

ad862ca851af7f05406b3d88d230b11a.jpg.webp

  1. C 就绪,并获得了 M 的控制权
  2. A 就绪,优先级比 C 高,CPU 优先处理 A
  3. A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 A 相等,即此时线程优先级关系是:A = C > B;C 继续读取 M
  4. C 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,轮到 A 对 M 进行修改,唤醒 A
  5. A 任务执行完成并挂起;B 在3-4之间已就绪,当时因优先级比 C 低,所以无法得到执行,而此时优先级比 C 高,CPU 优先唤醒处理 B
  6. B 任务执行完成并挂起,C 继续完成任务

三、互斥量制造了什么问题

3.1 错误地使用了 FIFO flag

​ 当用户需要避免上述线程优先级反转问题时,就需要用到互斥量对线程做同步。互斥量由 IPC 容器管理,因此线程想要获取互斥量时,需要在 IPC 中排队等待。IPC 的排队方式有两种:

  • RT_IPC_FLAG_FIFO:先进先出,队列按照先进先出方式排队
  • RT_IPC_FLAG_PRIO:优先级等待,队列将按照优先级进行排队,优先级高的等待线程将会插队排在优先级低的等待线程前

​ FIFO 属于非实时调度方式,所有排队等待的线程不再具有优先级的特性。然而,在创建/初始化(create/init)互斥量时,函数却允许用户使用 RT_IPC_FLAG_FIFO 参数,这会导致如下情形:

2832d07e4bdede3e851bb696ecd73a44.jpg.webp

  1. C 就绪,并获得了 M 的控制权
  2. B 就绪,优先级比 C 高,CPU 优先处理 B
  3. B 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 B 相等,即此时线程优先级关系是:A > B = C,B 进入 FIFO 队列,并排在第一位;C 继续读取 M
  4. A 就绪,优先级比 C 高,CPU 优先处理 A
  5. A 尝试获取 M 的控制权,因为 C 正在持有 M 的控制权,因此挂起等待;依据优先级继承协议,线程 C 的优先级被提升到与 A 相等,即此时线程优先级关系是:A = C > B,A 进入 FIFO 队列,根据先进先出原则,排在第二位,B 后面;C 继续读取 M
  6. C 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,根据 FIFO 队列,轮到 B 持有 M 的控制权,唤醒 B
  7. B 完成了读取 M 数据的操作,释放了 M 的控制权,优先级被恢复原样,根据 FIFO 队列,轮到 A 持有 M 的控制权,唤醒 A
  8. A 任务执行完成并挂起,B 继续任务
  9. 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。

c5a35e5cef2d41d0e65ff629fc33f257.jpg.webp

四、总结

​ 互斥量的诞生就是为了解决优先级反转的问题,但是错误地使用互斥量反而会让情况变得更糟糕。同时,这个 bug 相对隐蔽,不易被察觉,初学者(比如我)容易错误地使用,调试时也不容易复现。因此,修复此 bug 是很重要的。

五、结尾

​ 感谢 RTT 昨晚组织的特性解读会,的抽奖活动,让我终于中了一次奖哈哈哈!虽然是三等奖一条数据线,但这是我用这个抽奖小程序以来第一次中奖,真的很想吐槽那个抽奖小程序……

​ 另外,解读会有回放(虽然我暂时不知道在哪)。

猜你喜欢

转载自blog.csdn.net/rtthreadiotos/article/details/121008531