LWN:利用代理执行来解决优先级反转!

文章介绍了Linux内核如何处理优先级倒置问题,特别是当低优先级任务持有高优先级任务需要的资源时。代理执行是一种可能的解决方案,它涉及修改内核中的调度类,使得被阻塞的任务能将其调度上下文借给持有资源的任务,从而避免高优先级任务被延迟。文章还讨论了优先级继承、死线调度和相关挑战,以及内核如何跟踪和处理这些情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关注了就能看到更多这么棒的文章哦~

Addressing priority inversion with proxy execution

By Jonathan Corbet
June 9, 2023
DeepL assisted translation
https://lwn.net/Articles/934114/

当低优先级任务持有高优先级任务所需的资源时,就发生了所谓的优先级倒置(priority inversion),这会导致唯一可以运行的任务并不是原本应该最优先执行的任务。这个问题在实时环境中最为严重,但它其实在任何运行多个任务的系统中都会发生。Linux 内核提供的多种调度类(scheduling class)使得处理优先级倒置问题成为一个难题。最新版本的代理执行(proxy execution)patch 指向了可能的解决方案。

若要了解优先级反转,可以想象一下,有一个低优先级的后台任务获取了互斥锁(mutex)。如果另一个实时任务碰巧需要相同的互斥锁,就会发现自己被阻塞了,只能等待低优先级任务放弃它。如果出现另一个具有中等优先级的任务,那么它可能会导致低优先级任务无法执行,这意味着互斥锁不会被释放,实时任务也就会被继续被阻止执行,不知道什么时候才能运行。这正是优先级机制原本希望避免的问题。

优先级倒置的经典解决方案就是优先级继承(priority inheritance)。如果一个高优先级任务发现自己被另一个任务持有的资源所阻止,它将优先权借给拥有的任务,从而允许该任务尽快完成其工作并释放资源。Linux 内核长期以来一直支持优先级继承,但这并不能解决所有此类问题。Deadline scheduling 使情况变得更复杂了,因为它不是基于优先级的。由于在 deadline class 类别中的任务是没有优先级的,因此它不能将该优先级借给另一个任务。这样一来就导致优先级继承无法用于那些使用了 deadline scheduling 调度方式的任务。

内核开发人员已经研究这个问题一段时间了。例如,在 2019 年和 2020 年的调度和电源管理 (OSPM) 会议上讨论了这个问题。当前的 patch set 由 John Stultz 发布,但包含了许多开发人员的工作,显示了这项工作的当前状态。“proxy execution”的核心,是让被阻塞的进程将其整个调度上下文都借给另一个持有它所等待的资源的任务。

为了能够实现代理执行,调度程序需要确切地知道被阻止的任务正在等待哪个资源。task_struct 结构已经包含了一个名为 blocked_on 的 struct mutex 指针,正好用于此目的,但在当前内核中,只有在启用了 mutex debugging 机制的情况下才会把它编译进来。这组 patch 就修改此字段改为总是编译进来,以便始终可以进行这个信息的追踪。 mutex 结构已经有一个指针指向了此时哪个任务拥有此 mutex。这组 patch 也使该指针可供调度程序使用。这两个指针组合起来就可以让调度程序找到持有另一个任务所需资源的任务。

task_struct 结构包含了关于正在运行的任务的大量信息。这组 patch 里会识别出来这些信息,作为与调度相关的两种角色来使用:执行上下文(execution context)和调度上下文(scheduling context)。执行上下文包含了运行指定任务所需的信息,而调度上下文则描述了 CPU 调度器打算如何安排该任务。为了让这两个角色的逻辑分离开,在 rq (运行队列,run queue)结构中又给调度上下文而增加了一个 task_struct 指针。大多数情况下,某一个 run queue 条目的执行和调度上下文是相同的,但代理执行的情况下就可能会导致它们不一样了。

调度器的的 run queue 里存放了所有处于可运行状态(runnable)的任务,也就是说如果有可用的 CPU 的话它们就会在 CPU 上运行。当某个任务因为等待资源而被阻塞时,它会从这个运行队列中删除,直到它再次变得 runnable 为止。这组 patch 所做的更有趣的更改之一就是使被阻塞的任务仍然保留在运行队列中,即使它们实际上不可运行了。这会导致调度器会选择它希望运行的第一个任务(假设其资源可用),而不是它实际可以运行的第一个任务。

因此,这个机制可能会导致调度器尝试运行一个实际上无法运行的任务。此时调度器就可以把 CPU 提供给持有了阻塞资源的任务。使用这一套基础设施理论上就可以很容易实现代理执行了。如果所选任务不可运行,就按照其 blocked_on 指针来找到它正在等待的任务,把当前任务的调度上下文提供给该任务(从而事实上提高了其在运行队列中的位置),然后改为运行这个 task。当提升的任务释放了它持有的互斥锁时,它就会丢失这个优先任务调度上下文,于是更高优先级的任务将能够继续执行。问题就解决了。

当然,魔鬼总是存在于细节之中。持有所需互斥锁的任务本身可能会被另一个资源所阻塞,因此调度器根据 blocked-on 关系来查找一系列的任务。调度上下文中可能也会包括一些可以使用哪几个 CPU 的限制,因此如果一个 task 是作为 proxy 来运行的话就需要先迁移到另一个 CPU 上。调度器在正常的负载平衡工作中决定将任务迁移到另一个 CPU 之前,必须得注意这个代理执行的情况。CPU 时间统计也变得更加复杂,一个任务作为另一个任务的代理运行时使用的时间应计入正在运行的任务的耗时里,但是是从优先级较高的任务的时间片中拿过来,以保持调度公平性。

内核通常会努力将实时任务和 deadline 类型的任务都分散到系统的各个 CPU 中,以便所有任务都可以运行,但代理执行会将所涉及的任务绑定到同一个 CPU 上。如果由于隔离的需求导致要迁移其中一个 task,就必须同时要把这两个任务都迁移。并且这种情况下可能也需要考虑一连串的阻塞任务。该系列中最复杂的补丁之一就是试图解决这个问题。它不是创建 “某种复杂的数据结构” 来跟踪移动哪些任务,而是更改负载均衡代码来简单地搜索有哪些潜在的可移动任务(movable tasks)。这里的想法是,只要行为认为是正确的,就可以进行优化。

截至撰写本文时,这组 patch 还没有收到任何评论。似乎所有审阅者都阻塞在忙别的工作了。然而,鉴于这项工作的复杂性和悠久的历史,这个版本不太可能是最后一个版本。对于 CPU 调度器来说,哪怕是看似简单的更改也很难不产生一些细微的问题,并且这次这个改动又那么复杂。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值