轻量级 PI-futexes

我们称它们为轻量级的原因有 3 个

  • 在用户空间的快速路径中,启用 PI 的 futex 完全不涉及内核工作(或任何其他 PI 复杂性)。无需注册,无需额外的内核调用 - 只是用户空间中纯粹的快速原子操作。

  • 即使在慢速路径中,系统调用和调度模式也与普通 futexes 非常相似。

  • 内核中的 PI 实现围绕互斥抽象进行了简化,并具有严格的规则,以保持实现相对简单:只有一个所有者可以拥有锁(即不支持读写锁),只有所有者可以解锁锁,没有递归锁定等。

优先级继承 - 为什么?

简短的回答:用户空间 PI 有助于实现/提高用户空间应用程序的确定性。在最佳情况下,它可以帮助实现确定性和有界延迟。即使在最坏的情况下,PI 也会改善与锁定相关的应用程序延迟的统计分布。

更长的回复

首先,在多个任务之间共享锁是一种常见的编程技术,通常无法用无锁算法替代。正如我们在内核(它本身就是一个相当复杂的程序)中看到的那样,无锁结构与其说是常态,不如说是例外 - 当前共享数据结构的无锁代码与有锁代码的比例大约在 1:10 到 1:100 之间。无锁算法很难,并且无锁算法的复杂性通常会危及对所述代码进行健全审查的能力。也就是说,关键的 RT 应用程序通常选择锁结构来保护关键数据结构,而不是无锁算法。此外,在某些情况下(如共享硬件或其他资源限制),无锁访问在数学上是不可能的。

媒体播放器(如 Jack)就是一个合理的应用程序设计的示例,其中多个任务(具有多个优先级级别)共享短时间持有的锁:例如,高优先级音频播放线程与中优先级构造音频数据线程和低优先级显示色彩内容线程组合在一起。将视频和解码添加到组合中,我们就有了更多的优先级级别。

因此,一旦我们接受同步对象(锁)是生活中不可避免的事实,并且一旦我们接受多任务用户空间应用程序有非常合理的期望能够使用锁,我们就必须考虑如何为用户空间提供确定性锁定实现的选择。

大多数反对进行优先级继承的技术论点仅适用于内核空间锁。但是用户空间锁是不同的,在临界区中我们无法禁用中断或使任务不可抢占,因此“使用自旋锁”的论点不适用(用户空间自旋锁与其他用户空间锁定结构一样具有相同的优先级反转问题)。事实是,目前几乎唯一能够为用户空间锁(如基于 futex 的 pthread 互斥锁)实现良好确定性的技术是优先级继承。

目前(没有 PI),如果高优先级任务和低优先级任务共享一个锁 [对于大多数非平凡的 RT 应用程序来说,这是一个非常常见的情况],即使所有临界区都经过仔细编码以具有确定性(即,所有临界区持续时间都很短,并且仅执行有限数量的指令),内核也无法保证高优先级任务的任何确定性执行:任何中优先级任务都可以在低优先级任务持有共享锁并执行临界区时抢占低优先级任务,并可能无限期地延迟它。

实现

如前所述,启用 PI 的 pthread 互斥锁的用户空间快速路径完全不涉及内核工作 - 它们的行为与普通的基于 futex 的锁非常相似:值 0 表示未锁定,值 == TID 表示已锁定。(这与基于列表的健壮 futexes 使用的方法相同。)用户空间使用原子操作来锁定/解锁这些互斥锁,而无需进入内核。

为了处理慢速路径,我们添加了两个新的 futex 操作

  • FUTEX_LOCK_PI

  • FUTEX_UNLOCK_PI

如果锁获取快速路径失败,[即从 0 到 TID 的原子转换失败],则调用 FUTEX_LOCK_PI。内核完成所有剩余的工作:如果 futex 地址尚未附加 futex 队列,则代码查找拥有 futex 的任务 [它已将其自己的 TID 放入 futex 值],并将“PI 状态”结构附加到 futex 队列。pi_state 包括一个 rt-mutex,它是一个支持 PI 的基于内核的同步对象。“其他”任务成为 rt-mutex 的所有者,并且 FUTEX_WAITERS 位以原子方式在 futex 值中设置。然后此任务尝试锁定 rt-mutex,并在其上阻塞。一旦它返回,它就获得了互斥锁,并且它将 futex 值设置为其自己的 TID 并返回。用户空间没有其他工作要做 - 它现在拥有该锁,并且 futex 值包含 FUTEX_WAITERS | TID。

如果解锁端快速路径成功,[即用户空间设法对 futex 值进行 TID -> 0 原子转换],则不会触发内核工作。

如果解锁快速路径失败(因为设置了 FUTEX_WAITERS 位),则调用 FUTEX_UNLOCK_PI,并且内核代表用户空间解锁 futex - 它还会解锁附加的 pi_state->rt_mutex,从而唤醒任何潜在的等待者。

请注意,在这种方法下,与以前的 PI-futex 方法相反,不存在对 PI-futex 的先前“注册”。[由于 pthread 互斥锁的现有 ABI 属性,这无论如何都不太可能。]

此外,在此方案下,“健壮性”和“PI”是 futexes 的两个正交属性,并且所有四种组合都是可能的:futex、健壮-futex、PI-futex、健壮 + PI-futex。

有关优先级继承的更多详细信息,请参见支持 PI 的 RT-mutex 子系统